diff --git a/apps/desktop/native/computer-use/ComputerUse.swift b/apps/desktop/native/computer-use/ComputerUse.swift index de2a25b3898..f3129047b73 100644 --- a/apps/desktop/native/computer-use/ComputerUse.swift +++ b/apps/desktop/native/computer-use/ComputerUse.swift @@ -6,6 +6,9 @@ nonisolated(unsafe) var cancellationRequested: sig_atomic_t = 0 func checkCancellation() throws { if cancellationRequested != 0 { throw ComputerError("cancelled", "Computer-use operation was cancelled.") } } +/// Set by every input path the moment it reaches the target app; a request that fails before then +/// changed nothing, so its snapshot stays valid and the error reports `dispatchState: not_started`. +nonisolated(unsafe) var inputDispatched = false struct ComputerError: Error { let code: String @@ -43,6 +46,42 @@ func required(_ value: T?, _ name: String) throws -> T { func axCheck(_ result: AXError) throws { guard result == .success else { throw ComputerError("accessibility_error", "Accessibility operation failed (\(result.rawValue)); observe the app again before retrying.") } } +/// A press that opens a sheet, alert or modal panel holds the app's Accessibility reply until that UI +/// is up, so for a press `cannotComplete` after the messaging timeout means the request was delivered. +/// Any other failure may follow an effect, so it counts as dispatched unless Accessibility rejected the +/// request before the app saw it. +func dispatchAX(_ element: AXUIElement, press: Bool = false, _ operation: (AXUIElement) -> AXError) throws { + AXUIElementSetMessagingTimeout(element, 2) + let result = operation(element) + if ![.illegalArgument, .attributeUnsupported, .actionUnsupported, .notImplemented, .apiDisabled].contains(result) { inputDispatched = true } + if result == .success || result == .cannotComplete && press { return } + try axCheck(result) +} +/// Some apps (Calendar on macOS 26) answer AXPress on a menu item by opening its menu with the item +/// highlighted instead of running it. Return runs the highlighted item, as it would for a keyboard user. +func completeMenuPress(_ item: AXUIElement, pid: pid_t) async throws { + let submenu = (attribute(item, kAXChildrenAttribute) as? [AXUIElement] ?? []).contains { attribute($0, kAXRoleAttribute) as? String == kAXMenuRole } + guard attribute(item, kAXRoleAttribute) as? String == kAXMenuItemRole, !submenu else { return } + try await Task.sleep(for: .milliseconds(250)) + try checkCancellation() + // A press that ran its item closes the item's menu (a closed menu has no height). Return runs the + // highlighted item, so it is sent only while this item's own menu is open and the app highlights it. + guard let rawMenu = attribute(item, kAXParentAttribute), CFGetTypeID(rawMenu) == AXUIElementGetTypeID(), + case let menu = unsafeBitCast(rawMenu, to: AXUIElement.self), attribute(menu, kAXRoleAttribute) as? String == kAXMenuRole, + (elementRect(menu)?.height ?? 0) > 0, attribute(item, kAXSelectedAttribute) as? Bool == true else { return } + try postKey(pid: pid, code: 36) +} +/// A menu item's keyboard shortcut in macOS notation, such as ⌥⌘N, so the agent can use real shortcuts. +func menuShortcut(_ item: AXUIElement) -> String? { + guard let char = attribute(item, "AXMenuItemCmdChar") as? String, !char.isEmpty, char.unicodeScalars.allSatisfy({ $0.value >= 0x20 && $0.value != 0x7f }) else { return nil } + let modifiers = (attribute(item, "AXMenuItemCmdModifiers") as? NSNumber)?.intValue ?? 0 + var text = "" + if modifiers & 4 != 0 { text += "⌃" } + if modifiers & 2 != 0 { text += "⌥" } + if modifiers & 1 != 0 { text += "⇧" } + if modifiers & 8 == 0 { text += "⌘" } + return text + char +} func attribute(_ element: AXUIElement, _ name: String) -> CFTypeRef? { var value: CFTypeRef? return AXUIElementCopyAttributeValue(element, name as CFString, &value) == .success ? value : nil @@ -65,6 +104,36 @@ func requireNonSecure(_ element: AXUIElement) throws { func rectJSON(_ rect: CGRect) -> [String: Double] { ["x": rect.minX, "y": rect.minY, "width": rect.width, "height": rect.height] } +/// Whether keyboard focus is on `element` or an editor inside it, such as a combo box's field. +func holdsFocus(_ focused: CFTypeRef, _ element: AXUIElement) -> Bool { + guard CFGetTypeID(focused) == AXUIElementGetTypeID() else { return false } + var current: AXUIElement? = unsafeBitCast(focused, to: AXUIElement.self) + for _ in 0..<8 { + guard let candidate = current else { return false } + if CFEqual(candidate, element) { return true } + guard let parent = attribute(candidate, kAXParentAttribute), CFGetTypeID(parent) == AXUIElementGetTypeID() else { return false } + current = unsafeBitCast(parent, to: AXUIElement.self) + } + return false +} +/// Whether typed characters land as text: a text, combo or date control, or anything with an editable +/// text selection. Buttons, lists and web areas take characters as commands instead. +func takesText(_ element: AXUIElement) -> Bool { + if [kAXTextFieldRole, kAXTextAreaRole, kAXComboBoxRole, kAXDateFieldRole, kAXTimeFieldRole].contains(attribute(element, kAXRoleAttribute) as? String ?? "") { return true } + var settable: DarwinBoolean = false + return AXUIElementIsAttributeSettable(element, kAXSelectedTextRangeAttribute as CFString, &settable) == .success && settable.boolValue +} +/// The window holding `element`: the element itself when it is a window or sheet, which report no +/// window of their own, otherwise the window it reports. +func containingWindow(_ element: AXUIElement) -> AXUIElement? { + if [kAXWindowRole, kAXSheetRole].contains(attribute(element, kAXRoleAttribute) as? String ?? "") { return element } + guard let raw = attribute(element, kAXWindowAttribute), CFGetTypeID(raw) == AXUIElementGetTypeID() else { return nil } + return unsafeBitCast(raw, to: AXUIElement.self) +} +func axActions(_ element: AXUIElement) -> [String] { + var names: CFArray? + return AXUIElementCopyActionNames(element, &names) == .success ? names as? [String] ?? [] : [] +} func elementRect(_ element: AXUIElement) -> CGRect? { guard let rawPosition = attribute(element, kAXPositionAttribute), CFGetTypeID(rawPosition) == AXValueGetTypeID(), let rawSize = attribute(element, kAXSizeAttribute), CFGetTypeID(rawSize) == AXValueGetTypeID() else { return nil } @@ -176,6 +245,11 @@ final class Snapshot { let created = Date() var elements: [String: AXUIElement] = [:] var windowFrames: [String: CGRect] = [:] + /// The Accessibility window behind each observed windowId, so input checks compare identity, not + /// just bounds that two windows can share. + var windowElements: [String: AXUIElement] = [:] + /// The window this observation selected; coordinate input without a windowId targets it. + var selectedWindowId: String? var nodes: [[String: Any]] = [] var truncated = false init(app: NSRunningApplication) { pid = app.processIdentifier; launchDate = app.launchDate } @@ -211,6 +285,10 @@ final class Snapshot { queue.append(AXTraversalEntry(element: element, parent: depth == 0 ? nil : path[depth - 1], depth: depth, menu: false, priority: true)) } } + // The menu bar's titles and any open menu come before window content, which can exhaust the budget. + if let raw = attribute(root, kAXMenuBarAttribute), CFGetTypeID(raw) == AXUIElementGetTypeID() { + queue.append(AXTraversalEntry(element: unsafeBitCast(raw, to: AXUIElement.self), parent: root, depth: 1, menu: false, priority: true)) + } var menuCount = 0 while nodes.count < 2000 && Date().timeIntervalSince(created) < 8, let entry = queue.next() { try checkCancellation() @@ -219,10 +297,10 @@ final class Snapshot { if elements.values.contains(where: { CFEqual($0, element) }) { continue } let role = attribute(element, kAXRoleAttribute) as? String ?? "AXUnknown" let isMenu = entry.menu || [kAXMenuBarRole, kAXMenuRole, kAXMenuItemRole, kAXMenuBarItemRole].contains(role) - if isMenu && !entry.menu && !entry.priority { + if role == kAXMenuItemRole && !entry.menu && !entry.priority { queue.append(AXTraversalEntry(element: element, parent: entry.parent, depth: entry.depth, menu: true, priority: false)); continue } - if isMenu { + if isMenu && !entry.priority { guard menuCount < 80 else { truncated = true; continue } menuCount += 1 } @@ -232,6 +310,7 @@ final class Snapshot { let isSecure = secure(element) var labels = [kAXTitleAttribute, kAXDescriptionAttribute].compactMap { attribute(element, $0) as? String }.filter { !$0.isEmpty } if labels.isEmpty { labels = [kAXPlaceholderValueAttribute, kAXHelpAttribute].compactMap { attribute(element, $0) as? String }.filter { !$0.isEmpty } } + if role == kAXMenuItemRole, let shortcut = menuShortcut(element) { labels.append(shortcut) } if !isSecure, !labels.isEmpty { node["label"] = String(labels.joined(separator: " ").prefix(1024)) } if !isSecure, let raw = attribute(element, kAXValueAttribute) { if let value = raw as? String { node["value"] = String(value.prefix(2048)) } @@ -253,11 +332,23 @@ final class Snapshot { if !isSecure, entry.depth < 64 { let (children, childrenTruncated) = pagedElements(element, name: kAXChildrenAttribute, limit: 500) truncated = truncated || childrenTruncated - for child in children { queue.append(AXTraversalEntry(element: child, parent: element, depth: entry.depth + 1, menu: isMenu, priority: false)) } + // An open menu's items and their submenus come first; a closed menu (no height) keeps its items last. + let openMenu = role == kAXMenuRole ? (elementRect(element)?.height ?? 0) > 0 : isMenu && entry.priority + let deferChildren = !openMenu && (entry.menu || role == kAXMenuRole) + for child in children { queue.append(AXTraversalEntry(element: child, parent: element, depth: entry.depth + 1, menu: deferChildren, priority: openMenu)) } } else if entry.depth >= 64 { truncated = true } } if queue.hasPending || queue.truncated { truncated = true } } + /// The observed windowId whose window holds `element`; a sheet counts as the window it is attached to. + func windowId(holding element: AXUIElement) -> String? { + guard var window = containingWindow(element) else { return nil } + if attribute(window, kAXRoleAttribute) as? String == kAXSheetRole { guard let owner = sheetOwner(window) else { return nil }; window = owner } + return windowElements.first { CFEqual($0.value, window) }?.key + } + func observedWindows() -> [(element: AXUIElement, frame: CGRect)] { + windowFrames.compactMap { id, frame in windowElements[id].map { ($0, frame) } } + } func element(_ id: String?) throws -> AXUIElement { let id = try required(id, "elementId") guard let element = elements[id], attribute(element, kAXRoleAttribute) != nil else { throw ComputerError("stale_element", "Element is stale; observe the app again.") } @@ -315,9 +406,10 @@ func parseKey(_ key: String) throws -> (CGKeyCode, CGEventFlags) { let parts = key.lowercased().split(separator: "+").map(String.init) guard let name = parts.last else { throw ComputerError("invalid_arguments", "Key is empty.") } var flags: CGEventFlags = [] - for modifier in parts.dropLast() { switch modifier { case "cmd", "command": flags.insert(.maskCommand); case "shift": flags.insert(.maskShift); case "alt", "option": flags.insert(.maskAlternate); case "ctrl", "control": flags.insert(.maskControl); default: throw ComputerError("invalid_arguments", "Unknown key modifier.") } } + for modifier in parts.dropLast() { switch modifier { case "cmd", "command", "meta", "super": flags.insert(.maskCommand); case "shift": flags.insert(.maskShift); case "alt", "option", "opt": flags.insert(.maskAlternate); case "ctrl", "control": flags.insert(.maskControl); default: throw ComputerError("invalid_arguments", "Unknown key modifier.") } } let keys: [String: CGKeyCode] = ["a":0,"s":1,"d":2,"f":3,"h":4,"g":5,"z":6,"x":7,"c":8,"v":9,"b":11,"q":12,"w":13,"e":14,"r":15,"y":16,"t":17,"1":18,"2":19,"3":20,"4":21,"6":22,"5":23,"9":25,"7":26,"8":28,"0":29,"=":24,"-":27,"]":30,"[":33,"o":31,"u":32,"i":34,"p":35,"return":36,"enter":36,"l":37,"j":38,"'":39,";":41,"\\":42,",":43,"/":44,".":47,"`":50,"k":40,"n":45,"m":46,"tab":48,"space":49,"backspace":51,"escape":53,"delete":117,"left":123,"right":124,"down":125,"up":126,"home":115,"end":119,"pageup":116,"pagedown":121,"f1":122,"f2":120,"f3":99,"f4":118,"f5":96,"f6":97,"f7":98,"f8":100,"f9":101,"f10":109,"f11":103,"f12":111] - guard let code = keys[name] else { throw ComputerError("unsupported_key", "Unsupported key name.") }; return (code, flags) + let aliases = ["esc": "escape", "arrowleft": "left", "arrowright": "right", "arrowup": "up", "arrowdown": "down", "forwarddelete": "delete", "pgup": "pageup", "pgdn": "pagedown", "pgdown": "pagedown", "minus": "-", "equal": "=", "equals": "=", "comma": ",", "period": ".", "slash": "/", "semicolon": ";", "quote": "'", "backslash": "\\", "backtick": "`", "grave": "`", "leftbracket": "[", "rightbracket": "]"] + guard let code = keys[aliases[name] ?? name] else { throw ComputerError("unsupported_key", "Unsupported key name. Use names such as Enter, Tab, Escape, Backspace, Delete (forward delete), Left, PageUp, F5, or a single character, with Cmd/Shift/Alt/Ctrl modifiers.") }; return (code, flags) } func validateInputSteps(_ steps: [InputStep]) throws { guard (1...32).contains(steps.count) else { throw ComputerError("invalid_arguments", "Input sequence requires 1 to 32 steps.") } @@ -338,7 +430,7 @@ func sendEditorKey(element: AXUIElement, pid: pid_t, code: CGKeyCode, flags: CGE var settable: DarwinBoolean = false if AXUIElementIsAttributeSettable(element, kAXSelectedTextRangeAttribute as CFString, &settable) == .success, settable.boolValue, let value = attribute(element, kAXValueAttribute) as? String { var range = CFRange(location: 0, length: value.utf16.count) - if let selected = AXValueCreate(.cfRange, &range) { try axCheck(AXUIElementSetAttributeValue(element, kAXSelectedTextRangeAttribute as CFString, selected)); return } + if let selected = AXValueCreate(.cfRange, &range) { try dispatchAX(element) { AXUIElementSetAttributeValue($0, kAXSelectedTextRangeAttribute as CFString, selected) }; return } } } try postKey(pid: pid, code: code, flags: flags) @@ -347,25 +439,72 @@ func postKey(pid: pid_t, code: CGKeyCode, flags: CGEventFlags = [], text: String guard let source = CGEventSource(stateID: .privateState), let down = CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: true), let up = CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: false) else { throw ComputerError("input_failed", "Could not create keyboard event.") } down.flags = flags; up.flags = flags if let text { let chars = Array(text.utf16); chars.withUnsafeBufferPointer { down.keyboardSetUnicodeString(stringLength: chars.count, unicodeString: $0.baseAddress) } } + inputDispatched = true down.postToPid(pid); up.postToPid(pid) } func mapPoint(frame: CGRect, x: Double, y: Double) throws -> CGPoint { guard x.isFinite, y.isFinite, x >= 0, y >= 0, x < frame.width, y < frame.height else { throw ComputerError("invalid_coordinates", "Coordinates must lie within the selected window in points.") } return CGPoint(x: frame.minX + x, y: frame.minY + y) } -func requireForegroundTarget(pid: pid_t, windowID: String, point: CGPoint, frame: CGRect) throws { +func requireForegroundTarget(pid: pid_t, windowID: String, window: AXUIElement, point: CGPoint, frame: CGRect) throws { guard NSWorkspace.shared.frontmostApplication?.processIdentifier == pid else { throw ComputerError("foreground_required", "Coordinate mouse input requires the target app in front. Call activate_app, then observe again.") } guard try windowRect(windowID, pid: pid) == frame else { throw ComputerError("stale_window", "Window moved during input; observe it again.") } - guard let entries = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else { throw ComputerError("window_unavailable", "Cannot verify the target window.") } - let top = entries.first { entry in - guard (entry[kCGWindowAlpha as String] as? Double ?? 0) > 0, let bounds = entry[kCGWindowBounds as String] as? [String: Any], let rect = CGRect(dictionaryRepresentation: bounds as CFDictionary) else { return false } - return rect.contains(point) + guard let hit = elementAtPoint(point) else { throw ComputerError("window_occluded", "Cannot confirm which window receives this point; observe again before retrying.") } + var owner: pid_t = 0 + guard AXUIElementGetPid(hit, &owner) == .success, owner == pid else { throw ComputerError("window_occluded", "The target point is covered by another app's window; bring the intended window forward and observe again.") } + // The point must belong to the observed window (its sheets and popovers report it as their window), + // to one of the app's dialogs or floating panels above it, or to an open menu, which has no window. + // Anything unconfirmed is refused. + if let hitWindow = containingWindow(hit) { + guard acceptsInput(hitWindow, observed: [(window, frame)]) else { + throw ComputerError("window_occluded", "Another of this app's windows covers the target point; observe that window and target it instead.") + } + } else if ![kAXMenuRole, kAXMenuItemRole, kAXMenuBarRole, kAXMenuBarItemRole].contains(attribute(hit, kAXRoleAttribute) as? String ?? "") { + // Out-of-process panels (open and save) expose their contents without a window; they are + // reachable only inside the app's focused sheet, which must belong to the observed window. + let root = AXUIElementCreateApplication(pid) + guard let rawSheet = attribute(root, kAXFocusedWindowAttribute), CFGetTypeID(rawSheet) == AXUIElementGetTypeID(), + case let sheet = unsafeBitCast(rawSheet, to: AXUIElement.self), attribute(sheet, kAXRoleAttribute) as? String == kAXSheetRole, + elementRect(sheet)?.contains(point) == true, acceptsInput(sheet, observed: [(window, frame)]) else { + throw ComputerError("window_occluded", "Cannot confirm which window receives this point; observe again before retrying.") + } } - guard let top, (top[kCGWindowNumber as String] as? NSNumber)?.stringValue == windowID else { throw ComputerError("window_occluded", "The target point is covered by another window; bring the intended window forward and observe again.") } +} +/// The window a sheet is attached to: its parent, or for an out-of-process panel that reports no +/// parent, the window it reports as its own. +func sheetOwner(_ sheet: AXUIElement) -> AXUIElement? { + for name in [kAXParentAttribute, kAXWindowAttribute] { + if let raw = attribute(sheet, name), CFGetTypeID(raw) == AXUIElementGetTypeID() { + let owner = unsafeBitCast(raw, to: AXUIElement.self) + if attribute(owner, kAXRoleAttribute) as? String == kAXWindowRole { return owner } + } + } + return nil +} +/// Whether input aimed at an observed window may go to `window`: the observed window itself, or one of +/// the app's dialogs or floating panels above it. Another document window, or one whose kind cannot be +/// read, is refused. +func acceptsInput(_ window: AXUIElement, observed: [(element: AXUIElement, frame: CGRect)]) -> Bool { + if observed.contains(where: { CFEqual($0.element, window) && elementRect(window) == $0.frame }) { return true } + // A sheet (Go to Folder, an open or save panel) belongs to the window it is attached to. + if attribute(window, kAXRoleAttribute) as? String == kAXSheetRole { return sheetOwner(window).map { acceptsInput($0, observed: observed) } ?? false } + let auxiliary: Set = [kAXDialogSubrole, kAXSystemDialogSubrole, kAXFloatingWindowSubrole, kAXSystemFloatingWindowSubrole] + return (attribute(window, kAXSubroleAttribute) as? String).map(auxiliary.contains) ?? false +} +/// The element that receives a click at a global point. Notification Center and the Dock keep +/// full-screen click-through windows above every app, so the topmost window in the window list is not +/// the click target; Accessibility hit-testing follows the window server's routing. Without an answer +/// the target is unknown, and the caller refuses the input. +func elementAtPoint(_ point: CGPoint) -> AXUIElement? { + let system = AXUIElementCreateSystemWide() + AXUIElementSetMessagingTimeout(system, 0.25) + var hit: AXUIElement? + guard AXUIElementCopyElementAtPosition(system, Float(point.x), Float(point.y), &hit) == .success else { return nil } + return hit } final class CoordinateDispatch { private(set) var hasDispatched = false - func willDispatch() { hasDispatched = true } + func willDispatch() { hasDispatched = true; inputDispatched = true } func classify(_ error: ComputerError) -> ComputerError { ComputerError(error.code, error.message, dispatchState: hasDispatched ? nil : "not_started") } @@ -376,9 +515,12 @@ func coordinateAction(_ operation: (CoordinateDispatch) async throws -> Void) as do { try await operation(dispatch) } catch let error as ComputerError { throw dispatch.classify(error) } } -func postMouse(pid: pid_t, type: CGEventType, point: CGPoint, button: CGMouseButton, count: Int = 1, windowID: String? = nil, frame: CGRect? = nil, release: Bool = false, dispatch: CoordinateDispatch) throws { +func postMouse(pid: pid_t, type: CGEventType, point: CGPoint, button: CGMouseButton, count: Int = 1, windowID: String? = nil, window: AXUIElement? = nil, frame: CGRect? = nil, release: Bool = false, dispatch: CoordinateDispatch) throws { guard let windowID, let frame else { throw ComputerError("invalid_window", "Mouse input requires an observed app window.") } - if !release { try requireForegroundTarget(pid: pid, windowID: windowID, point: point, frame: frame) } + if !release { + guard let window else { throw ComputerError("invalid_window", "Mouse input requires an observed app window.") } + try requireForegroundTarget(pid: pid, windowID: windowID, window: window, point: point, frame: frame) + } guard let source = CGEventSource(stateID: .privateState), let event = CGEvent(mouseEventSource: source, mouseType: type, mouseCursorPosition: point, mouseButton: button) else { throw ComputerError("input_failed", "Could not create mouse event.") } event.setIntegerValueField(.mouseEventClickState, value: Int64(count)) dispatch.willDispatch() @@ -417,7 +559,7 @@ final class Driver { guard let frame = elementRect(try snapshot.element(elementId)) else { throw ComputerError("element_unavailable", "Element has no usable frame.") } return CGPoint(x: frame.midX, y: frame.midY) } - let windowId = try required(p.windowId, "windowId") + let windowId = try required(p.windowId ?? snapshot.selectedWindowId, "windowId") let frame = try windowRect(windowId, pid: app.processIdentifier) guard snapshot.windowFrames[windowId] == frame else { throw ComputerError("stale_window", "Window moved or changed; observe it again.") } let x = try required(end ? p.toX : p.x, end ? "toX" : "x"); let y = try required(end ? p.toY : p.y, end ? "toY" : "y") @@ -468,10 +610,22 @@ final class Driver { let app = try await appFor(p.bundleId, launch: true); let snapshot = Snapshot(app: app) let preferredFrame = try p.windowId.map { try windowRect($0, pid: app.processIdentifier) } try await snapshot.read(preferredWindowFrame: preferredFrame) - let axWindowFrames = snapshot.elements.values.filter { attribute($0, kAXRoleAttribute) as? String == kAXWindowRole }.compactMap(elementRect) - let windows = windowsFor(app.processIdentifier).prefix(100).compactMap { entry -> [String: Any]? in - guard let id = entry[kCGWindowNumber as String] as? NSNumber, let bounds = entry[kCGWindowBounds as String] as? [String: Any], let rect = CGRect(dictionaryRepresentation: bounds as CFDictionary), rect.width > 0, rect.height > 0, axWindowFrames.contains(rect) else { return nil } + let axWindows = snapshot.elements.values.filter { attribute($0, kAXRoleAttribute) as? String == kAXWindowRole } + let axWindowFrames = axWindows.compactMap(elementRect) + let entries = windowsFor(app.processIdentifier).prefix(100) + func bounds(_ entry: [String: Any]) -> CGRect? { (entry[kCGWindowBounds as String] as? [String: Any]).flatMap { CGRect(dictionaryRepresentation: $0 as CFDictionary) } } + func onScreen(_ entry: [String: Any]) -> Bool { entry[kCGWindowIsOnscreen as String] as? Bool == true } + // A background tab is an off-screen window with the bounds of the tab in front, which is the + // only one Accessibility shows, so it can be neither observed nor targeted. + let onScreenRects = entries.filter(onScreen).compactMap(bounds) + let windows = entries.compactMap { entry -> [String: Any]? in + guard let id = entry[kCGWindowNumber as String] as? NSNumber, let rect = bounds(entry), rect.width > 0, rect.height > 0, axWindowFrames.contains(rect), onScreen(entry) || !onScreenRects.contains(rect) else { return nil } snapshot.windowFrames[id.stringValue] = rect + // Public APIs match a window-list entry to its Accessibility window only by bounds; when + // two windows share bounds that match is ambiguous, so neither gets an identity and + // input aimed at either is refused. An off-screen (minimized) window takes no input either. + let matches = axWindows.filter { elementRect($0) == rect } + if matches.count == 1, onScreen(entry) { snapshot.windowElements[id.stringValue] = matches[0] } var result: [String: Any] = ["windowId": id.stringValue, "title": entry[kCGWindowName as String] as? String ?? ""] if let title = result["title"] as? String { result["title"] = String(title.prefix(4096)) } result.merge(rectJSON(rect)) { _, new in new }; return result @@ -479,15 +633,14 @@ final class Driver { if snapshots.count >= 16 { snapshots.removeAll() } snapshots[app.bundleIdentifier!] = snapshot let root = AXUIElementCreateApplication(app.processIdentifier) - var focusedFrame: CGRect? - if let focused = attribute(root, kAXFocusedWindowAttribute), CFGetTypeID(focused) == AXUIElementGetTypeID() { focusedFrame = elementRect(unsafeBitCast(focused, to: AXUIElement.self)) } - let focusedID = snapshot.windowFrames.first { $0.value == focusedFrame }?.key + let focusedID = attribute(root, kAXFocusedWindowAttribute).flatMap { CFGetTypeID($0) == AXUIElementGetTypeID() ? snapshot.windowId(holding: unsafeBitCast($0, to: AXUIElement.self)) : nil } for index in snapshot.nodes.indices { - if let element = snapshot.elements[snapshot.nodes[index]["elementId"] as? String ?? ""], attribute(element, kAXRoleAttribute) as? String == kAXWindowRole, let frame = elementRect(element), let windowID = snapshot.windowFrames.first(where: { $0.value == frame })?.key { snapshot.nodes[index]["windowId"] = windowID } + if let element = snapshot.elements[snapshot.nodes[index]["elementId"] as? String ?? ""], attribute(element, kAXRoleAttribute) as? String == kAXWindowRole, let windowID = snapshot.windowId(holding: element) { snapshot.nodes[index]["windowId"] = windowID } else if let parentID = snapshot.nodes[index]["parentId"] as? String, let parent = snapshot.nodes.first(where: { $0["elementId"] as? String == parentID }), let windowID = parent["windowId"] as? String { snapshot.nodes[index]["windowId"] = windowID } } guard let selectedWindowId = p.windowId ?? focusedID ?? windows.first?["windowId"] as? String else { throw ComputerError("app_window_unavailable", "No accessible app window is available. Open a window, then observe the app again.") } guard snapshot.windowFrames[selectedWindowId] != nil else { throw ComputerError("window_unavailable", "Selected window does not belong to the app.") } + snapshot.selectedWindowId = selectedWindowId var result: [String: Any] = ["kind": "state", "windowId": selectedWindowId, "snapshotId": snapshot.id, "bundleId": app.bundleIdentifier!, "nodes": snapshot.nodes, "windows": windows, "truncated": snapshot.truncated] if p.includeScreenshot == true { do { @@ -504,33 +657,56 @@ final class Driver { try requireAccessibility() let app = try await appFor(p.bundleId); let bundle = try required(app.bundleIdentifier, "bundleId") guard let snapshot = snapshots[bundle], snapshot.id == p.snapshotId, snapshot.pid == app.processIdentifier, snapshot.launchDate == app.launchDate, Date().timeIntervalSince(snapshot.created) < 60 else { throw ComputerError("stale_snapshot", "Observe the app again before acting.") } - snapshots.removeValue(forKey: bundle) + inputDispatched = false + do { + let result = try await act(request, app: app, bundle: bundle, snapshot: snapshot) + snapshots.removeValue(forKey: bundle) + return result + } catch let error as ComputerError { + // Input that reached the app may have changed it, so only an untouched app keeps its references. + if inputDispatched { snapshots.removeValue(forKey: bundle); throw error } + // Only a malformed or unsupported request leaves the observation itself usable. + let requestError = ["invalid_arguments", "invalid_coordinates", "invalid_window", "unsupported_action", "unsupported_key", "element_unavailable", "secure_element"].contains(error.code) + throw ComputerError(error.code, error.message + (requestError ? " Nothing was sent to the app; this snapshot is still valid." : ""), dispatchState: "not_started") + } catch { + snapshots.removeValue(forKey: bundle); throw error + } + } + func act(_ request: Request, app: NSRunningApplication, bundle: String, snapshot: Snapshot) async throws -> [String: Any] { + let p = request.params let pid = app.processIdentifier switch request.method { case "perform_action": let element = try snapshot.element(p.elementId); let action = try required(p.accessibilityAction, "accessibilityAction") var names: CFArray?; try axCheck(AXUIElementCopyActionNames(element, &names)) guard (names as? [String] ?? []).contains(action) else { throw ComputerError("unsupported_action", "Action was not advertised by the selected element.") } - try axCheck(AXUIElementPerformAction(element, action as CFString)) + // Actions that open UI (a sheet, a menu) hold the reply like a press; others fail as uncertain on timeout. + let opensUI = [kAXPressAction, kAXPickAction, kAXShowMenuAction].contains(action) + try dispatchAX(element, press: opensUI) { AXUIElementPerformAction($0, action as CFString) } + if action == kAXPressAction || action == kAXPickAction { try await completeMenuPress(element, pid: pid) } case "set_value": let element = try snapshot.element(p.elementId); let value = try required(p.value, "value") guard value.utf16.count <= 32000 else { throw ComputerError("invalid_arguments", "Value exceeds 32000 UTF-16 units.") } var writable: DarwinBoolean = false; try axCheck(AXUIElementIsAttributeSettable(element, kAXValueAttribute as CFString, &writable)) - guard writable.boolValue else { throw ComputerError("unsupported_action", "This element does not support setting its value.") } - try axCheck(AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, value as CFString)) + guard writable.boolValue else { throw ComputerError("unsupported_action", "This element does not support setting its value; click it and use type_text instead.") } + try dispatchAX(element) { AXUIElementSetAttributeValue($0, kAXValueAttribute as CFString, value as CFString) } case "click": let button = p.button ?? "left"; let count = p.clickCount ?? 1 guard ["left", "right"].contains(button), (1...3).contains(count) else { throw ComputerError("invalid_arguments", "Invalid button or clickCount.") } - if p.elementId != nil && button == "left" && count == 1 { try axCheck(AXUIElementPerformAction(try snapshot.element(p.elementId), kAXPressAction as CFString)) } - else { + if p.elementId != nil && button == "left" && count == 1, let element = try? snapshot.element(p.elementId), axActions(element).contains(kAXPressAction) { + try dispatchAX(element, press: true) { AXUIElementPerformAction($0, kAXPressAction as CFString) } + try await completeMenuPress(element, pid: pid) + } else { try await coordinateAction { dispatch in let position = try point(p, app: app, snapshot: snapshot); let mouse: CGMouseButton = button == "right" ? .right : .left - let windowID = try required(p.windowId ?? snapshot.windowFrames.first(where: { $0.value.contains(position) })?.key, "windowId") + // Coordinates are window-local to the requested (or selected) window. An element targets its + // own window; an out-of-process panel's content reports none and the selected window's sheet check applies. + let windowID = try required(p.windowId ?? p.elementId.flatMap { snapshot.windowId(holding: try snapshot.element($0)) } ?? snapshot.selectedWindowId, "windowId") let frame = try required(snapshot.windowFrames[windowID], "window frame") for click in 1...count { try checkCancellation() - try postMouse(pid: pid, type: button == "right" ? .rightMouseDown : .leftMouseDown, point: position, button: mouse, count: click, windowID: windowID, frame: frame, dispatch: dispatch) - try postMouse(pid: pid, type: button == "right" ? .rightMouseUp : .leftMouseUp, point: position, button: mouse, count: click, windowID: windowID, frame: frame, release: true, dispatch: dispatch) + try postMouse(pid: pid, type: button == "right" ? .rightMouseDown : .leftMouseDown, point: position, button: mouse, count: click, windowID: windowID, window: snapshot.windowElements[windowID], frame: frame, dispatch: dispatch) + try postMouse(pid: pid, type: button == "right" ? .rightMouseUp : .leftMouseUp, point: position, button: mouse, count: click, windowID: windowID, window: snapshot.windowElements[windowID], frame: frame, release: true, dispatch: dispatch) } } } @@ -539,20 +715,33 @@ final class Driver { let steps = request.method == "type_text" ? [InputStep(action: "type_text", text: try required(p.text, "text"), key: nil)] : try required(p.steps, "steps") try validateInputSteps(steps) let root = AXUIElementCreateApplication(pid) - guard let rawWindow = attribute(element, kAXWindowAttribute), CFGetTypeID(rawWindow) == AXUIElementGetTypeID(), let frame = elementRect(unsafeBitCast(rawWindow, to: AXUIElement.self)), let windowID = snapshot.windowFrames.first(where: { $0.value == frame })?.key else { throw ComputerError("window_unavailable", "Editor does not belong to an observed window.") } - guard [kAXTextAreaRole, kAXTextFieldRole].contains(attribute(element, kAXRoleAttribute) as? String ?? "") else { throw ComputerError("unsupported_action", "Input requires an observed text editor.") } - var editable: DarwinBoolean = false - guard AXUIElementIsAttributeSettable(element, kAXValueAttribute as CFString, &editable) == .success, editable.boolValue else { throw ComputerError("unsupported_action", "Observed editor is read-only.") } + // Keystrokes go to whatever holds keyboard focus: a text control (terminals included, read-only + // or not), or a container whose focused editor already holds it, such as a web composer. + let targetTakesText = takesText(element) + guard targetTakesText || attribute(root, kAXFocusedUIElementAttribute).map({ holdsFocus($0, element) }) == true else { + throw ComputerError("unsupported_action", "This element does not take text. Type into a text field, text area, combo box or date field, or use press_key for shortcuts.") + } if request.method == "input_sequence", p.activateFirst == true { try await activate(app) } - if attribute(root, kAXFocusedUIElementAttribute).map({ CFEqual($0, element) }) != true { + // Activation can move focus, so whether the editor holds it is read after activating. + if attribute(root, kAXFocusedUIElementAttribute).map({ holdsFocus($0, element) }) != true { try axCheck(AXUIElementSetAttributeValue(element, kAXFocusedAttribute as CFString, kCFBooleanTrue)) try await Task.sleep(for: .milliseconds(50)) } + let observedWindows = snapshot.observedWindows() + // The first editor found holding focus receives the whole input, so text never follows focus elsewhere. + var editor: AXUIElement? func validateFocus() throws { try checkCancellation(); try requireNonSecure(element) - guard try windowRect(windowID, pid: pid) == frame else { throw ComputerError("stale_window", "Editor window geometry changed; observe again.") } - guard let current = attribute(root, kAXFocusedUIElementAttribute), CFEqual(current, element) else { throw ComputerError("focus_changed", "The exact editor is no longer focused; remaining input was not dispatched. Observe again.") } - guard let focusedWindow = attribute(root, kAXFocusedWindowAttribute), CFEqual(focusedWindow, rawWindow) else { throw ComputerError("focus_changed", "The editor's exact window is not focused; remaining input was not dispatched. Observe again.") } + // The editor must still sit in an observed window (or a dialog above it) at the observed frame. + guard let window = containingWindow(element), acceptsInput(window, observed: observedWindows) else { + throw ComputerError("stale_window", "The editor's window moved or is not an observed window; observe again.") + } + guard let current = attribute(root, kAXFocusedUIElementAttribute), holdsFocus(current, element), editor.map({ CFEqual($0, current) }) ?? true else { throw ComputerError("focus_changed", "The exact editor is no longer focused; remaining input was not dispatched. Observe again.") } + // Keystrokes land on the focused element, so it (or the text control targeted) must take text. + let receiver = unsafeBitCast(current, to: AXUIElement.self) + try requireNonSecure(receiver) + guard targetTakesText || takesText(receiver) else { throw ComputerError("unsupported_action", "Keyboard focus is not in a text input; no text was sent. Click into the text field, or use press_key for shortcuts.") } + editor = receiver } // AX focus requests and app activation may settle asynchronously. Wait without // posting input or requesting focus again; every eventual input keeps the same guard. @@ -594,10 +783,36 @@ final class Driver { } if request.method == "input_sequence" { return ["kind": "action", "action": request.method, "bundleId": bundle, "dispatched": true, "verified": false, "sequence": ["completedSteps": completed, "totalSteps": steps.count]] } case "press_key": - let windowId = try required(p.windowId, "windowId"); let frame = try windowRect(windowId, pid: pid) - guard snapshot.windowFrames[windowId] == frame else { throw ComputerError("stale_window", "Observe the target window again.") } + // Without a windowId the key goes to whichever observed window has focus. + let frame = try p.windowId.map { windowId in + let frame = try windowRect(windowId, pid: pid) + guard snapshot.windowFrames[windowId] == frame else { throw ComputerError("stale_window", "Observe the target window again.") } + return frame + } let root = AXUIElementCreateApplication(pid) - guard let rawWindow = attribute(root, kAXFocusedWindowAttribute), CFGetTypeID(rawWindow) == AXUIElementGetTypeID(), elementRect(unsafeBitCast(rawWindow, to: AXUIElement.self)) == frame else { throw ComputerError("focus_failed", "Requested window is not the app's focused window.") } + // A targeted key first moves keyboard focus to that element, as clicking into it would. + if let elementId = p.elementId { + let element = try snapshot.element(elementId) + if attribute(root, kAXFocusedUIElementAttribute).map({ holdsFocus($0, element) }) != true { + try axCheck(AXUIElementSetAttributeValue(element, kAXFocusedAttribute as CFString, kCFBooleanTrue)) + } + // Focus can settle asynchronously; the key is sent only once this element holds it. + var focused = false + for _ in 0..<10 { + try checkCancellation() + if attribute(root, kAXFocusedUIElementAttribute).map({ holdsFocus($0, element) }) == true { focused = true; break } + try await Task.sleep(for: .milliseconds(50)) + } + guard focused else { throw ComputerError("focus_changed", "The element did not take keyboard focus; no key was sent. Observe again.") } + } + // Keys go to the app's key window: the observed window, or a dialog, panel or popover above it, + // but never another document window the snapshot did not observe. + guard let rawWindow = attribute(root, kAXFocusedWindowAttribute), CFGetTypeID(rawWindow) == AXUIElementGetTypeID() else { throw ComputerError("focus_failed", "The app has no focused window to receive keys; click a window or call activate_app, then observe again.") } + let focusedWindow = unsafeBitCast(rawWindow, to: AXUIElement.self) + let targets: [(element: AXUIElement, frame: CGRect)] + if let windowId = p.windowId, let frame { targets = snapshot.windowElements[windowId].map { [($0, frame)] } ?? [] } + else { targets = snapshot.observedWindows() } + guard acceptsInput(focusedWindow, observed: targets) else { throw ComputerError("focus_failed", "Another document window has keyboard focus; observe that window and target it instead.") } if let focused = attribute(root, kAXFocusedUIElementAttribute), CFGetTypeID(focused) == AXUIElementGetTypeID() { try requireNonSecure(unsafeBitCast(focused, to: AXUIElement.self)) } let (code, flags) = try parseKey(required(p.key, "key")) var selectedAll = false @@ -606,7 +821,7 @@ final class Driver { var settable: DarwinBoolean = false if AXUIElementIsAttributeSettable(element, kAXSelectedTextRangeAttribute as CFString, &settable) == .success, settable.boolValue, let value = attribute(element, kAXValueAttribute) as? String { var range = CFRange(location: 0, length: value.utf16.count) - if let selectedRange = AXValueCreate(.cfRange, &range) { try axCheck(AXUIElementSetAttributeValue(element, kAXSelectedTextRangeAttribute as CFString, selectedRange)); selectedAll = true } + if let selectedRange = AXValueCreate(.cfRange, &range) { try dispatchAX(element) { AXUIElementSetAttributeValue($0, kAXSelectedTextRangeAttribute as CFString, selectedRange) }; selectedAll = true } } } if !selectedAll { try postKey(pid: pid, code: code, flags: flags) } @@ -615,30 +830,33 @@ final class Driver { let position = try point(p, app: app, snapshot: snapshot); let dx = p.deltaX ?? 0; let dy = p.deltaY ?? 0 guard dx.isFinite, dy.isFinite, abs(dx) <= 10000, abs(dy) <= 10000 else { throw ComputerError("invalid_arguments", "Scroll deltas exceed bounds.") } guard let event = CGEvent(scrollWheelEvent2Source: CGEventSource(stateID: .privateState), units: .pixel, wheelCount: 2, wheel1: Int32(-dy), wheel2: Int32(-dx), wheel3: 0) else { throw ComputerError("input_failed", "Could not create scroll event.") } - let windowID = try required(p.windowId ?? snapshot.windowFrames.first(where: { $0.value.contains(position) })?.key, "windowId") + // Coordinates are window-local to the requested (or selected) window; an element targets its own. + let windowID = try required(p.windowId ?? p.elementId.flatMap { snapshot.windowId(holding: try snapshot.element($0)) } ?? snapshot.selectedWindowId, "windowId") let frame = try required(snapshot.windowFrames[windowID], "window frame") - try requireForegroundTarget(pid: pid, windowID: windowID, point: position, frame: frame) + try requireForegroundTarget(pid: pid, windowID: windowID, window: try required(snapshot.windowElements[windowID], "observed window"), point: position, frame: frame) event.location = position; dispatch.willDispatch(); event.post(tap: .cghidEventTap) } case "drag": try await coordinateAction { dispatch in let start = try point(p, app: app, snapshot: snapshot); let end = try point(p, app: app, snapshot: snapshot, end: true) - let windowID = try required(p.windowId, "windowId"); let frame = try required(snapshot.windowFrames[windowID], "window frame") - try postMouse(pid: pid, type: .leftMouseDown, point: start, button: .left, windowID: windowID, frame: frame, dispatch: dispatch) + let windowID = try required(p.windowId ?? snapshot.selectedWindowId, "windowId"); let frame = try required(snapshot.windowFrames[windowID], "window frame") + try postMouse(pid: pid, type: .leftMouseDown, point: start, button: .left, windowID: windowID, window: snapshot.windowElements[windowID], frame: frame, dispatch: dispatch) var lastPoint = start - defer { try? postMouse(pid: pid, type: .leftMouseUp, point: lastPoint, button: .left, windowID: windowID, frame: frame, release: true, dispatch: dispatch) } + defer { try? postMouse(pid: pid, type: .leftMouseUp, point: lastPoint, button: .left, windowID: windowID, window: snapshot.windowElements[windowID], frame: frame, release: true, dispatch: dispatch) } try await Task.sleep(for: .milliseconds(10)) for step in 1...10 { try checkCancellation() let amount = CGFloat(step) / 10 let position = CGPoint(x: start.x + (end.x - start.x) * amount, y: start.y + (end.y - start.y) * amount) - try postMouse(pid: pid, type: .leftMouseDragged, point: position, button: .left, windowID: windowID, frame: frame, dispatch: dispatch) + try postMouse(pid: pid, type: .leftMouseDragged, point: position, button: .left, windowID: windowID, window: snapshot.windowElements[windowID], frame: frame, dispatch: dispatch) lastPoint = position try await Task.sleep(for: .milliseconds(10)) } } default: throw ComputerError("unknown_method", "Unknown method.") } + // Give the app a moment to render the result so the follow-up observation shows it. + try await Task.sleep(for: .milliseconds(200)) return ["kind": "action", "action": request.method, "bundleId": bundle, "dispatched": true, "verified": false] } } diff --git a/apps/desktop/native/computer-use/README.md b/apps/desktop/native/computer-use/README.md index e2f81daaf80..3c8d22e8537 100644 --- a/apps/desktop/native/computer-use/README.md +++ b/apps/desktop/native/computer-use/README.md @@ -2,19 +2,19 @@ Independent native implementation using public Accessibility, Core Graphics and ScreenCaptureKit APIs. Requires macOS 14+. Package `ComputerUse.swift` as `Sim Computer Use.app/Contents/MacOS/SimComputerUse` with this directory's `Info.plist`; sign the nested app with the desktop distribution identity so TCC grants survive upgrades. -Compile each architecture with `swiftc -parse-as-library -O -target arm64-apple-macosx14.0 ComputerUse.swift -o SimComputerUse` (repeat for `x86_64`, combine with `lipo`). No Apple Events, shell execution, private APIs, global event listeners or clipboard access is used. Coordinate mouse input posts public foreground events only after checking the active app, observed window geometry and topmost target point. +Compile each architecture with `swiftc -parse-as-library -O -target arm64-apple-macosx14.0 ComputerUse.swift -o SimComputerUse` (repeat for `x86_64`, combine with `lipo`). No Apple Events, shell execution, private APIs, global event listeners or clipboard access is used. Coordinate mouse input posts public foreground events only after checking the active app, observed window geometry and that Accessibility hit-testing routes the point to the target app. ## Protocol Persistent newline-delimited JSON over stdin/stdout: `{id,method,params}` returns `{id,result}` or `{id,error:{code,message,dispatchState?}}`. The producer contract is `worker/packages/contracts/src/computer-use.ts` in mothership, copied into the desktop bridge by contract sync. `request_permission` is a local onboarding method, never a model tool; params are `{permission:"accessibility"|"screenCapture"}`. Status/preflight never triggers prompts. Errors go through the protocol, and stdout contains no logs. -The helper observes up to 2000 accessibility nodes (depth 64, queue 4000, traversal deadline 8 seconds, each AX call timeout 250ms). Selected-window and focused-element ancestry are prioritized; menu traversal is deferred and capped at 80 nodes. Electron accessibility is enabled using its documented `AXManualAccessibility` attribute. The helper observes 100 windows and 1000 apps. Requests are bounded to 128 KiB during ingestion. Text/value input is capped at 32000 UTF-16 units. State references are per-process, per-app, expire after 60 seconds, and are consumed by every attempted action. Restarts invalidate all references. Screen captures exclude window shadows, preserve the selected window’s coordinate geometry, and are capped to 1600 pixels on the longest side and 8 MiB encoded. Complete JSON replies are capped at 16 MiB: oversized observations omit screenshots first, then truncate accessibility nodes, while preserving action outcomes. Screenshots are optional and failure is reported in `screenshotError` without discarding accessibility state. macOS 26 uses the screenshot-specific capture API; the macOS 14/15 API path includes only the selected window in a display filter and crops its exact frame. That older path requires the window to fit within one display and reports `capture_geometry_unavailable` otherwise. +The helper observes up to 2000 accessibility nodes (depth 64, queue 4000, traversal deadline 8 seconds, each AX call timeout 250ms). Selected-window and focused-element ancestry are prioritized. The menu bar and menus are read in place, so an open menu's items and their submenus come first; items of closed menus are read last, capped at 80 menu nodes. Electron accessibility is enabled using its documented `AXManualAccessibility` attribute. The helper observes 100 windows and 1000 apps. Requests are bounded to 128 KiB during ingestion. Text/value input is capped at 32000 UTF-16 units. State references are per-process, per-app, expire after 60 seconds, and are consumed by every action that reaches the app; a request that fails before sending anything reports `dispatchState: "not_started"` and keeps them valid. An Accessibility failure counts as sent unless Accessibility rejected the request before the app saw it (unsupported attribute or action, illegal argument). Restarts invalidate all references. Screen captures exclude window shadows, preserve the selected window’s coordinate geometry, and are capped to 1600 pixels on the longest side and 8 MiB encoded. Complete JSON replies are capped at 16 MiB: oversized observations omit screenshots first, then truncate accessibility nodes, while preserving action outcomes. Screenshots are optional and failure is reported in `screenshotError` without discarding accessibility state. macOS 26 uses the screenshot-specific capture API; the macOS 14/15 API path includes only the selected window in a display filter and crops its exact frame. That older path requires the window to fit within one display and reports `capture_geometry_unavailable` otherwise. -Coordinates in action inputs are window-local points; node frames are global screen points. Window bounds must still match the observed snapshot. App PID and launch time must match. Secure fields and descendants are unavailable as action targets; secure values and labels are suppressed. Sim's own apps and System Settings are protected targets. `get_app_state` can open an installed bundle ID without activating it. `list_apps` discovers installed apps under `/Applications`, `/System/Applications`, and `~/Applications` (bounded to 5000 filesystem entries and depth 3), merging running regular apps; installed apps omit `pid`. +Coordinates in action inputs are window-local points; node frames are global screen points. Coordinate input without a `windowId` targets the window the snapshot selected, and an element target uses its own window; `press_key` with an `elementId` focuses that element first. Window bounds must still match the observed snapshot. App PID and launch time must match. Secure fields and descendants are unavailable as action targets; secure values and labels are suppressed. Sim's own apps and System Settings are protected targets. `get_app_state` can open an installed bundle ID without activating it. `list_apps` discovers installed apps under `/Applications`, `/System/Applications`, and `~/Applications` (bounded to 5000 filesystem entries and depth 3), merging running regular apps; installed apps omit `pid`. -AXPress and AXSetValue are direct semantic operations. Keyboard events target the app PID. Cmd+A uses the focused editor's writable accessibility selection range where supported. Coordinate mouse input requires the target app in front; `activate_app` makes that focus change explicit and confirms it. Click/drag/scroll validate the foreground process, unchanged window geometry and topmost window at the point. Some apps ignore background keyboard events. `dispatched:true,verified:false` means only dispatch succeeded; the agent must observe again to establish the outcome. No foreground activation fallback is hidden inside the helper. Native key codes currently assume the US layout for shortcuts; literal text uses Unicode events. Unsupported shortcuts fail explicitly. +AXPress and AXSetValue are direct semantic operations. Keyboard events target the app PID. Cmd+A uses the focused editor's writable accessibility selection range where supported. Coordinate mouse input requires the target app in front; `activate_app` makes that focus change explicit and confirms it. Click/drag/scroll validate the foreground process, unchanged window geometry and the app that receives the point; the topmost window-list entry is not the click target because Notification Center and the Dock keep full-screen click-through windows. A hit on a window's own background counts as that window. Background tabs and minimized windows share bounds with the window in front, so they take no coordinate input. An action that opens UI (AXPress, AXPick, AXShowMenu) and times out while the app shows a sheet, modal or menu (`cannotComplete`) counts as dispatched; any other action that times out fails as uncertain. A menu-item press that leaves the item's own menu open with the app highlighting that item (Calendar on macOS 26) is completed with Return. Sheets, including Go to Folder and out-of-process open and save panels whose contents report no window, count as the window they are attached to. Menu item labels carry their shortcuts (for example ⇧⌘N). Some apps ignore background keyboard events. `dispatched:true,verified:false` means only dispatch succeeded; the agent must observe again to establish the outcome. No foreground activation fallback is hidden inside the helper. Native key codes currently assume the US layout for shortcuts; literal text uses Unicode events. Unsupported shortcuts fail explicitly. -`input_sequence` executes 1–32 key/text steps against one observed editable element. It validates every step before dispatch and checks the same editor, window, and secure-input ancestry between steps. Enter can continue into another step when the same editor and window retain focus. Actual focus loss, Tab, or navigation shortcuts end the sequence before later input. Explicit `activateFirst: true` activates the target app inside the batch before input; it never enables automatic refocusing between steps. Partial execution reports completed steps and an error; callers must observe before retrying. Every mutation is followed by fresh state under the same authorization and queue entry; `observeAfter` optionally requests a screenshot. Observation failure is reported separately so the mutation is never replayed. The model receives a compact, bounded projection of the native snapshot; omitted nodes are reported explicitly. +`type_text` and `input_sequence` target a text control (text fields and areas, terminals, combo boxes, date fields, or anything with an editable text selection), or a container whose focused editor already holds keyboard focus, such as a web composer; keystrokes are sent only while focus is in a text input. `input_sequence` executes 1–32 key/text steps against one such element. It validates every step before dispatch and checks the same editor, window, and secure-input ancestry between steps. Enter can continue into another step when the same editor and window retain focus. Actual focus loss, Tab, or navigation shortcuts end the sequence before later input. Explicit `activateFirst: true` activates the target app inside the batch before input; it never enables automatic refocusing between steps. Partial execution reports completed steps and an error; callers must observe before retrying. Every mutation is followed by fresh state under the same authorization and queue entry; `observeAfter` optionally requests a screenshot. Observation failure is reported separately so the mutation is never replayed. The model receives a compact, bounded projection of the native snapshot; omitted nodes are reported explicitly. ## Cancellation diff --git a/apps/desktop/native/computer-use/tests/live.py b/apps/desktop/native/computer-use/tests/live.py index 0bac9b9f3d0..0a3bacb4eab 100644 --- a/apps/desktop/native/computer-use/tests/live.py +++ b/apps/desktop/native/computer-use/tests/live.py @@ -53,6 +53,14 @@ def mutate(method, snapshot, **params): return state() +def refused(method, snapshot, **params): + try: + call(method, bundleId=bundle, snapshotId=snapshot['snapshotId'], **params) + except AssertionError as error: + return json.loads(str(error).split(': ', 1)[1]) + raise AssertionError(method + ' was not refused') + + def coordinate(snapshot, node, fx=0.5, fy=0.5): window = next(w for w in snapshot['windows'] if w['windowId'] == snapshot['windowId']) return {'windowId': window['windowId'], 'x': node['x'] - window['x'] + node['width'] * fx, 'y': node['y'] - window['y'] + node['height'] * fy} @@ -106,10 +114,20 @@ def coordinate(snapshot, node, fx=0.5, fy=0.5): scroll = find(snapshot, role='AXScrollArea') snapshot = mutate('scroll', snapshot, **coordinate(snapshot, scroll), deltaX=0, deltaY=180) assert any((node.get('value', '').startswith('Scroll: ') and node['value'] != 'Scroll: 0') or (node.get('label', '').startswith('Scroll: ') and node['label'] != 'Scroll: 0') for node in snapshot['nodes']), 'Scroll did not move fixture content' + slider = find(snapshot, role='AXSlider') + error = refused('type_text', snapshot, elementId=slider['elementId'], text=' ') + assert error['code'] == 'unsupported_action' and error['dispatchState'] == 'not_started', error + assert find(state(), role='AXSlider')['value'] == slider['value'], 'Text reached a control that does not take text' + snapshot = state() + snapshot = mutate('click', snapshot, elementId=find(snapshot, role='AXTextField', label='Fixture editable text')['elementId']) + snapshot = mutate('type_text', snapshot, elementId=find(snapshot, role='AXWindow')['elementId'], text=' window') + assert find(snapshot, role='AXTextField', label='Fixture editable text')['value'].endswith(' window'), 'Window target did not type into its focused editor' + window = next(w for w in snapshot['windows'] if w['windowId'] == snapshot['windowId']) + snapshot = mutate('click', snapshot, x=window['width'] * 0.75, y=8) button = find(snapshot, role='AXButton', label='Increment') snapshot = mutate('perform_action', snapshot, elementId=button['elementId'], accessibilityAction='AXPress') assert any(f'Count: {baseline + 2}' in node.get('value', '') or f'Count: {baseline + 2}' in node.get('label', '') for node in snapshot['nodes']), 'Explicit AX action did not increment' - print('PASS: real fixture state, secure suppression, AX click/set/action, Unicode typing, keyboard chord, coordinate drag and scrolling; every mutation verified in fresh state; screenshot=' + ('verified' if permissions['screenRecording'] else 'BLOCKED')) + print('PASS: real fixture state, secure suppression, AX click/set/action, Unicode typing, keyboard chord, coordinate drag and scrolling, text refused by a slider, a window target typing into its focused editor, a title-bar click; every mutation verified in fresh state; screenshot=' + ('verified' if permissions['screenRecording'] else 'BLOCKED')) finally: if fixture: fixture.terminate() diff --git a/apps/desktop/src/main/browser-agent/post-action-observation.test.ts b/apps/desktop/src/main/browser-agent/post-action-observation.test.ts index 629bdc29a7b..3938df6c7fb 100644 --- a/apps/desktop/src/main/browser-agent/post-action-observation.test.ts +++ b/apps/desktop/src/main/browser-agent/post-action-observation.test.ts @@ -94,4 +94,25 @@ describe('post-action observation', () => { }) } ) + + it.each([ + 'browser_click_at', + 'browser_insert_text', + 'browser_select_option', + 'browser_set_checked', + 'browser_drag', + ] as const)('observes after %s instead of refusing the call', async (tool) => { + await expect( + withPostActionObservation( + tool, + { observe: {} }, + async () => ({ dispatched: true }), + async () => ({ outline: 'After' }), + () => {} + ) + ).resolves.toMatchObject({ + dispatched: true, + observation: { ok: true, result: { outline: 'After' } }, + }) + }) }) diff --git a/apps/desktop/src/main/browser-agent/post-action-observation.ts b/apps/desktop/src/main/browser-agent/post-action-observation.ts index b00b94f7b2b..3b2d7554d64 100644 --- a/apps/desktop/src/main/browser-agent/post-action-observation.ts +++ b/apps/desktop/src/main/browser-agent/post-action-observation.ts @@ -3,13 +3,19 @@ import { getErrorMessage } from '@sim/utils/errors' import { isRecordLike } from '@sim/utils/object' import { ToolError } from '@/main/browser-agent/errors' +/** Actions that accept observe; models carry it over from click and type to these. */ const OBSERVABLE_ACTIONS: ReadonlySet = new Set([ 'browser_click', + 'browser_click_at', 'browser_type', + 'browser_insert_text', 'browser_press_key', 'browser_fill_form', + 'browser_select_option', + 'browser_set_checked', 'browser_scroll', 'browser_hover', + 'browser_drag', 'browser_batch', ]) diff --git a/apps/desktop/src/main/computer-use/service.test.ts b/apps/desktop/src/main/computer-use/service.test.ts index 4f38c05db78..67d81dff4f4 100644 --- a/apps/desktop/src/main/computer-use/service.test.ts +++ b/apps/desktop/src/main/computer-use/service.test.ts @@ -296,6 +296,28 @@ describe('native computer use authority and lifecycle', () => { await expect(click('s1', 'chat', 'repeat')).rejects.toThrow('stale') }) + it('keeps a snapshot usable after an action that sent nothing to the app', async () => { + const { state, native, click } = setup() + await state() + native.request.mockRejectedValueOnce( + new ComputerUseError({ + code: 'window_occluded', + message: 'Covered.', + dispatchState: 'not_started', + }) + ) + await expect(click('s1', 'chat', 'refused')).rejects.toThrow('Covered.') + await expect(click('s1', 'chat', 'retry')).resolves.toMatchObject({ dispatched: true }) + }) + + it('consumes a snapshot after an action whose dispatch is unknown', async () => { + const { state, native, click } = setup() + await state() + native.request.mockRejectedValueOnce(new Error('Computer Use helper exited.')) + await expect(click('s1', 'chat', 'lost')).rejects.toThrow('exited') + await expect(click('s1', 'chat', 'repeat')).rejects.toThrow('stale') + }) + it.each(['app', 'window'])('rejects a follow-up snapshot from a different %s', async (target) => { const { state, service, native, click } = setup() await state() diff --git a/apps/desktop/src/main/computer-use/service.ts b/apps/desktop/src/main/computer-use/service.ts index cf4a78868e4..3f9cfd3fd02 100644 --- a/apps/desktop/src/main/computer-use/service.ts +++ b/apps/desktop/src/main/computer-use/service.ts @@ -1,7 +1,8 @@ -import type { - ComputerUseActivity, - ComputerUseAppPermission, - ComputerUseStatus, +import { + type ComputerUseActivity, + type ComputerUseAppPermission, + ComputerUseError, + type ComputerUseStatus, } from '@sim/desktop-bridge' import { type ComputerUseInput, @@ -179,7 +180,18 @@ export class ComputerUseService { return Promise.reject(new Error('Computer Use has too many queued actions.')) if (this.queued.has(toolCallId)) return Promise.reject(new Error('This Computer Use action is already running.')) - const input = ComputerUseSchema.parse(raw) + // A newer server can send arguments this build predates; nothing has been queued yet. + const parsed = ComputerUseSchema.safeParse(raw) + if (!parsed.success) + return Promise.reject( + new ComputerUseError({ + code: 'invalid_arguments', + message: + 'This desktop app does not support these Computer Use arguments; update Sim Desktop.', + dispatchState: 'not_started', + }) + ) + const input = parsed.data const work: ToolWork = { toolCallId, scopeId, input, controller: new AbortController() } this.queued.set(toolCallId, work) const result = this.tail.then(() => this.run(work)) @@ -227,16 +239,45 @@ export class ComputerUseService { } this.check(work) const owner = 'snapshotId' in input ? this.snapshots.get(input.snapshotId) : undefined + const elementId = 'elementId' in input ? input.elementId : undefined + /** + * Element and coordinate calls observe the window they acted on. An untargeted key keeps the + * focused-window default, so a key that closes the window still returns fresh state. + */ const windowId = - 'windowId' in input - ? input.windowId - : 'elementId' in input - ? (owner?.elementWindows.get(input.elementId) ?? owner?.windowId) - : undefined + ('windowId' in input ? input.windowId : undefined) ?? + (elementId + ? (owner?.elementWindows.get(elementId) ?? owner?.windowId) + : 'x' in input + ? owner?.windowId + : undefined) if ('snapshotId' in input) this.snapshots.delete(input.snapshotId) - const params = - 'observeAfter' in input ? omit(input, ['action', 'observeAfter']) : omit(input, ['action']) - const result = await this.deps.native.request(input.action, params) + /** Mutations accept includeScreenshot as shorthand for observeAfter.includeScreenshot. */ + const isMutation = input.action !== 'get_app_state' + const observeAfter = { + ...('observeAfter' in input ? input.observeAfter : {}), + ...(isMutation && 'includeScreenshot' in input && input.includeScreenshot !== undefined + ? { includeScreenshot: input.includeScreenshot } + : {}), + } + const fields: Record = input + const params = isMutation + ? omit(fields, ['action', 'observeAfter', 'includeScreenshot']) + : omit(fields, ['action']) + let result: ComputerUseResult + try { + result = await this.deps.native.request(input.action, params) + } catch (error) { + /** The helper keeps a snapshot nothing was sent through, so its ownership stays too. */ + if ( + owner && + 'snapshotId' in input && + error instanceof ComputerUseError && + error.details.dispatchState === 'not_started' + ) + this.snapshots.set(input.snapshotId, owner) + throw error + } this.check(work) if (result.kind === 'state') this.rememberSnapshot(work, result, windowId) if (result.kind === 'action') { @@ -250,7 +291,7 @@ export class ComputerUseService { const observation = await this.deps.native.request('get_app_state', { bundleId: input.bundleId, ...(windowId ? { windowId } : {}), - ...('observeAfter' in input ? input.observeAfter : {}), + ...observeAfter, }) this.check(work) if (observation.kind !== 'state') diff --git a/apps/sim/app/(landing)/changelog/components/changelog-link/changelog-link.tsx b/apps/sim/app/(landing)/changelog/components/changelog-link/changelog-link.tsx new file mode 100644 index 00000000000..b7ad5ed3b27 --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-link/changelog-link.tsx @@ -0,0 +1,82 @@ +import type { ComponentPropsWithoutRef } from 'react' +import { cn } from '@sim/emcn' +import Link from 'next/link' +import { SITE_URL } from '@/lib/core/utils/urls' +import { blockTypeToIconMap, INTEGRATIONS } from '@/lib/integrations' +import { IntegrationIcon } from '@/app/(landing)/integrations/components/integration-icon' +import { getProviderBySlug } from '@/app/(landing)/models/utils' + +interface ChangelogLinkProps extends ComponentPropsWithoutRef<'a'> {} + +const SITE_HOST = new URL(SITE_URL).hostname +const SITE_APEX = SITE_HOST.replace(/^www\./, '') +const INTEGRATIONS_BY_PATH = new Map( + INTEGRATIONS.map((integration) => [`/integrations/${integration.slug}`, integration]) +) +const INTEGRATIONS_BY_DOCS = new Map( + INTEGRATIONS.map((integration) => [integration.docsUrl, integration]) +) + +/** Decorates catalog links with the same brand marks used on their destination pages. */ +export function ChangelogLink({ href, children, className, ...props }: ChangelogLinkProps) { + let url: URL | undefined + try { + if (href) url = new URL(href, SITE_URL) + } catch {} + + const isSite = url?.hostname === SITE_HOST || url?.hostname === SITE_APEX + const isExternal = url && url.hostname !== SITE_APEX && !url.hostname.endsWith(`.${SITE_APEX}`) + const pathname = url?.pathname.replace(/\/$/, '') + const integration = isSite + ? INTEGRATIONS_BY_PATH.get(pathname ?? '') + : url && INTEGRATIONS_BY_DOCS.get(`${url.origin}${pathname}`) + const providerSlug = isSite ? pathname?.match(/^\/models\/([^/]+)(?:\/[^/]+)?$/)?.[1] : undefined + const ProviderIcon = providerSlug ? getProviderBySlug(providerSlug)?.icon : undefined + const isHeadingAnchor = className?.split(' ').includes('anchor') + const icon = !isHeadingAnchor && (integration || ProviderIcon) + + const content = ( + <> + {icon ? ( + + ) : null} + {icon ? {children} : children} + + ) + const linkClassName = cn( + 'text-inherit no-underline', + icon && 'inline-flex max-w-full items-center gap-1 align-baseline', + !isHeadingAnchor && !icon && 'font-medium', + !isHeadingAnchor && 'focus-visible:outline-2 focus-visible:outline-offset-4', + className + ) + + return href && isSite ? ( + + {content} + + ) : ( + + {content} + + ) +} diff --git a/apps/sim/app/(landing)/changelog/components/changelog-link/index.ts b/apps/sim/app/(landing)/changelog/components/changelog-link/index.ts new file mode 100644 index 00000000000..3beaa640fee --- /dev/null +++ b/apps/sim/app/(landing)/changelog/components/changelog-link/index.ts @@ -0,0 +1 @@ +export { ChangelogLink } from './changelog-link' diff --git a/apps/sim/app/(landing)/changelog/components/index.ts b/apps/sim/app/(landing)/changelog/components/index.ts index 3e41a2dd2d5..d1167d01b06 100644 --- a/apps/sim/app/(landing)/changelog/components/index.ts +++ b/apps/sim/app/(landing)/changelog/components/index.ts @@ -3,5 +3,6 @@ export { ChangelogArticle } from './changelog-article' export { ChangelogHeader } from './changelog-header' export { ChangelogImage } from './changelog-image' export { ChangelogLayout } from './changelog-layout' +export { ChangelogLink } from './changelog-link' export { ChangelogList } from './changelog-list' export { ChangelogVideo } from './changelog-video' diff --git a/apps/sim/app/(landing)/comparisons/utils.ts b/apps/sim/app/(landing)/comparisons/utils.ts index 1c896d501fe..97f31b944b4 100644 --- a/apps/sim/app/(landing)/comparisons/utils.ts +++ b/apps/sim/app/(landing)/comparisons/utils.ts @@ -100,13 +100,14 @@ export function buildBottomLine(competitor: CompetitorProfile): ComparisonVerdic : `Choose ${competitor.name} if its specific strengths, documented above, matter more to your team than an AI-native, self-hostable workspace.` return { - chooseSim: `Choose Sim if you want an open-source, self-hostable AI workspace that treats AI agents as first-class citizens: native multi-LLM support, real-time multiplayer editing, environment promotion (dev/qa/prod), human-in-the-loop approvals, and enterprise governance (SSO, credential-level permissions, audit logs) built in rather than bolted on.`, + chooseSim: `Choose Sim if you want an open-source, self-hostable AI workspace that treats AI agents as first-class citizens: native multi-LLM support, real-time multiplayer editing, human-in-the-loop approvals, and, on the Enterprise plan, environment promotion (dev/qa/prod) and governance (SSO, credential-level permissions, audit logs) built in rather than bolted on.`, chooseCompetitor, } } /** - * Builds the FAQ set for a "Sim vs {Competitor}" page. Answer-first, each + * Builds the FAQ set for a "Sim vs {Competitor}" page: the profile's + * hand-written FAQs first, then the generated ones. Answer-first, each * question/answer pair is independently quotable per the landing GEO rules, * and every answer names "Sim" and the competitor explicitly. Every answer * draws on a real, sourced {@link Fact} field rather than a generic claim, @@ -116,9 +117,10 @@ export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFa const name = competitor.name const facts = competitor.facts const faqs: ComparisonFaq[] = [ + ...(competitor.faqs ?? []), { question: `Is Sim a good alternative to ${name}?`, - answer: `Sim is an open-source AI workspace where teams build, deploy, and manage AI agents visually, conversationally, or with code. ${ensurePeriod(competitor.oneLiner)} Teams considering a switch typically weigh licensing (Sim is Apache 2.0 and self-hostable), pricing model, and how AI-native the platform's agent-building experience is.`, + answer: `Sim is an open-source AI workspace where teams build, deploy, and manage AI agents visually, conversationally, or with code. ${ensurePeriod(competitor.oneLiner)} Teams considering a switch typically weigh licensing (Sim's core is Apache 2.0 and self-hostable), pricing model, and how AI-native the platform's agent-building experience is.`, }, { question: `What is the main difference between Sim and ${name}?`, @@ -126,7 +128,7 @@ export function buildComparisonFaqs(competitor: CompetitorProfile): ComparisonFa }, { question: `Does Sim support self-hosting compared to ${name}?`, - answer: `Sim can be self-hosted via Docker or Kubernetes under an Apache 2.0 license, in addition to a managed cloud-hosted plan. ${name}'s self-hosting position: ${ensurePeriod(firstSentence(facts.platform.selfHostOption.value))}`, + answer: `Sim can be self-hosted via Docker or Kubernetes with an Apache 2.0-licensed core, in addition to a managed cloud-hosted plan. Enterprise features such as SSO and audit logs require an Enterprise license. ${name}'s self-hosting position: ${ensurePeriod(firstSentence(facts.platform.selfHostOption.value))}`, }, { question: `How does Sim's pricing compare to ${name}?`, diff --git a/apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx b/apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx index 5929f4d7c42..9af4e9a7393 100644 --- a/apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx +++ b/apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react' -import { Avatar, AvatarFallback, AvatarImage } from '@sim/emcn' +import { Avatar, AvatarFallback, AvatarImage, cn } from '@sim/emcn' import Image from 'next/image' import Link from 'next/link' import { FAQ } from '@/lib/content/faq' @@ -140,7 +140,16 @@ export function ContentPostPage({
-
+
{post.faq && post.faq.length > 0 ? : null}
diff --git a/apps/sim/app/(landing)/integrations/data/seo-content.ts b/apps/sim/app/(landing)/integrations/data/seo-content.ts index 31ef6da86cb..0ec2760bd51 100644 --- a/apps/sim/app/(landing)/integrations/data/seo-content.ts +++ b/apps/sim/app/(landing)/integrations/data/seo-content.ts @@ -22,9 +22,337 @@ export const INTEGRATION_SEO: Record = { h1: 'GitHub Workflow Automation and Integration', tagline: 'Build GitHub automation and trigger AI workflows from GitHub events.', overview: - 'Use Sim’s GitHub integration to run GitHub automation inside one AI workspace. Get pull request details, post pull request and issue comments, fetch repository and commit data, and trigger workflows from pull requests, comments, pushes, releases, and GitHub Actions events. Build GitHub workflow automation for code reviews, changelog generation, engineering digests, release operations, and documentation updates.', + 'Use Sim’s GitHub integration to run GitHub automation inside one AI workspace. Read pull requests, files, and commits; create and update issues, pull requests, branches, files, and releases; post comments and pull request reviews; and start workflows from issue, pull request, push, release, and GitHub Actions events. For example, a PR Opened trigger can start a workflow that fetches the changed files and diffs with Get PR files, then passes them to an Agent block that posts a pull request review with specific suggestions. An Issue Opened trigger can have an agent read the issue, apply labels and an assignee, and post a triage comment. A Release Published trigger can compare commits since the previous tag and send a changelog to Slack.', triggersIntro: - 'Connect a GitHub webhook to Sim and your agent runs the instant an event happens, no polling, no delay.', + 'Sim supports GitHub webhook triggers for Issue Opened, Issue Closed, Issue Comment, PR Opened, PR Closed, PR Merged, PR Comment, PR Reviewed, Code Push, Release Published, and Actions Workflow Run. Copy the webhook URL Sim generates into your repository’s Settings > Webhooks, and the workflow starts when GitHub delivers the event instead of waiting for a scheduled check.', + comparison: { + id: 'github-comparison', + heading: 'Sim vs. Zapier and n8n for GitHub', + intro: + 'Zapier and n8n both provide GitHub integrations for general-purpose automation. Sim focuses on workflows where an Agent block reads a pull request, issue, or diff before a GitHub block comments, reviews, labels, or opens a pull request. Zapier or n8n may suit a GitHub workflow that relays events without an AI step.', + columns: ['Sim', 'Zapier', 'n8n'], + rows: [ + { + label: 'Trigger model', + values: [ + { + text: 'Real-time webhook', + }, + { + text: 'Polling', + href: 'https://zapier.com/apps/github/integrations', + }, + { + text: 'Webhook', + href: 'https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.githubtrigger', + }, + ], + }, + { + label: 'AI step before acting', + values: [ + { + text: 'Agent block, built in', + }, + { + text: 'Requires a separate AI step or app', + href: 'https://zapier.com/apps/github/integrations/chatgpt', + }, + { + text: 'Requires a separate AI Agent node', + href: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent', + }, + ], + }, + { + label: 'Free to start', + values: [ + { + text: 'Yes', + }, + { + text: 'Yes, 100 tasks a month on two-step Zaps', + href: 'https://zapier.com/pricing', + }, + { + text: 'Yes, when self-hosted', + href: 'https://docs.n8n.io/n8n-community-license', + }, + ], + }, + { + label: 'Open pull requests', + values: [ + { + text: 'Create pull request operation', + }, + { + text: 'Create Pull Request action', + href: 'https://zapier.com/apps/github/integrations', + }, + { + text: 'Pull Request Create operation', + href: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.github', + }, + ], + }, + ], + conclusion: + 'Choose Sim when an AI agent should read the pull request or issue and decide what to comment, label, or change, rather than relay each GitHub event unchanged.', + }, + faqs: [ + { + question: 'Can a Sim agent open pull requests, triage issues, and comment on GitHub?', + answer: + 'Yes. Sim’s GitHub block includes Create pull request, Create issue comment, Create PR comment, Create PR review, Add issue labels, and Add issue assignees, and an Agent block can choose which to run. Issue Opened and PR Opened triggers start the workflow when GitHub delivers the event, so triage and review comments do not wait on a schedule.', + }, + { + question: 'What GitHub events can start a Sim workflow?', + answer: + 'Sim has GitHub triggers for Issue Opened, Issue Closed, Issue Comment, PR Opened, PR Closed, PR Merged, PR Comment, PR Reviewed, Code Push, Release Published, and Actions Workflow Run. Each one uses a webhook URL that you add in your repository’s Settings > Webhooks, with an optional webhook secret.', + }, + { + question: 'Can Sim use a GitHub MCP server?', + answer: + 'Sim can connect a remote MCP server that uses Streamable HTTP and give its tools to an Agent block or run them through the MCP block; local stdio servers are not supported. Sim does not ship its own GitHub MCP server, and its built-in GitHub block covers pull requests, issues, files, branches, releases, and Actions without one.', + }, + { + question: 'How do I connect GitHub to Sim?', + answer: + 'Add a GitHub block to a workflow and enter a GitHub personal access token in its credential field. Choose an operation such as Get PR details or Create issue, or switch the block to trigger mode and pick a GitHub event. Test the workflow, then deploy it to run on live events.', + }, + { + question: 'What is the difference between Sim and Zapier for GitHub?', + answer: + 'Zapier’s GitHub triggers poll for new data, and its free plan allows two-step Zaps, so a Zap with an AI step between the trigger and the action needs a paid plan. Sim receives GitHub webhooks directly and includes Agent blocks in every workflow. Choose Sim when an agent should read code or issue context before acting.', + }, + ], + }, + shopify: { + title: 'Shopify Automation with Sim', + description: + 'Build Shopify automation in Sim. Manage products, orders, customers, inventory, and fulfillment with AI agents. Free to start.', + keywords: ['shopify automation', 'shopify inventory automation', 'shopify integration'], + h1: 'Shopify Automation with Sim', + tagline: + 'Build Shopify automation with AI agents in Sim. Manage products, variants, orders, customers, inventory, collections, and fulfillment.', + overview: + 'Connect Shopify to Sim with OAuth and run Shopify automation from one AI workspace. Create and update products and variants, read and update orders, manage customers, check inventory levels by location, adjust inventory quantities, and create fulfillments with tracking. For example, a scheduled workflow can list inventory items, have an Agent block flag variants whose available quantity is below a threshold, and post the list to Slack. An agent can review recent orders and add tags or notes with Update Order. A restock workflow can apply an Adjust Inventory change at a location after an agent checks the received quantity.', + comparison: { + id: 'shopify-comparison', + heading: 'Sim vs. Zapier and n8n for Shopify', + intro: + 'Zapier and n8n both provide Shopify integrations, and both offer Shopify triggers that Sim does not have. Sim focuses on workflows where an Agent block reads inventory, order, or customer data and decides what to change before a Shopify block acts. Zapier or n8n may suit a workflow that must start the moment a Shopify event fires.', + columns: ['Sim', 'Zapier', 'n8n'], + rows: [ + { + label: 'Trigger model', + values: [ + { + text: 'Scheduled; no native Shopify trigger', + }, + { + text: 'Instant for most events, including Updated Inventory', + href: 'https://zapier.com/apps/shopify/integrations', + }, + { + text: 'Webhook', + href: 'https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.shopifytrigger', + }, + ], + }, + { + label: 'AI step before acting', + values: [ + { + text: 'Agent block, built in', + }, + { + text: 'Requires a separate AI step or app', + href: 'https://zapier.com/apps/shopify/integrations/chatgpt', + }, + { + text: 'Requires a separate AI Agent node', + href: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent', + }, + ], + }, + { + label: 'Free to start', + values: [ + { + text: 'Yes', + }, + { + text: 'Yes, 100 tasks a month on two-step Zaps', + href: 'https://zapier.com/pricing', + }, + { + text: 'Yes, when self-hosted', + href: 'https://docs.n8n.io/n8n-community-license', + }, + ], + }, + { + label: 'Adjust inventory', + values: [ + { + text: 'Adjust Inventory operation', + }, + { + text: 'Update Inventory Quantity action', + href: 'https://zapier.com/apps/shopify/integrations', + }, + { + text: 'No inventory operation in the Shopify node', + href: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.shopify', + }, + ], + }, + ], + conclusion: + 'Choose Sim when an AI agent should review inventory and order data and decide what to flag or change on a schedule, rather than react to each Shopify event with a fixed action.', + }, + faqs: [ + { + question: 'Can a Sim agent update Shopify inventory and flag low stock?', + answer: + 'Yes, on a schedule. Sim’s Shopify block can list inventory items with available and on-hand quantities, get an inventory level at a location, and change a variant’s quantity by a set amount with Adjust Inventory. A Schedule trigger can run an Agent block that flags low-stock variants and posts them to Slack, because Sim has no Shopify trigger that fires when inventory changes.', + }, + { + question: 'What Shopify order and inventory operations does Sim support?', + answer: + 'Sim supports Get Order, List Orders, Update Order (note, tags, and email), and Cancel Order, plus List Inventory Items, Get Inventory Level, Adjust Inventory, and List Locations. Fulfillment operations include Create Fulfillment, List Fulfillment Orders, and Update Fulfillment Tracking. Product, variant, customer, and collection operations are also available.', + }, + { + question: 'Does Sim have Shopify triggers?', + answer: + 'No. Sim does not have a native Shopify trigger, so Shopify workflows in Sim usually start from a Schedule trigger or another trigger in the workflow. If a workflow must start the moment an order or inventory event fires, Zapier and n8n both offer Shopify triggers.', + }, + { + question: 'How do I connect Shopify to Sim?', + answer: + 'Add a Shopify block to a workflow and connect your store with OAuth. Choose an operation such as List Orders or Adjust Inventory, test the workflow, and deploy it. Orders normally cover the last 60 days, and older orders and protected customer fields require Shopify approval.', + }, + { + question: 'What is the difference between Sim and Zapier for Shopify?', + answer: + 'Zapier offers instant Shopify triggers and inventory actions, but its free plan allows two-step Zaps, so a Zap with an AI step between the trigger and the action needs a paid plan. Sim includes Agent blocks in every workflow and runs Shopify checks on a schedule. Choose Sim when an agent should decide which products, orders, or quantities need attention.', + }, + ], + }, + snowflake: { + title: 'Snowflake Automation with Sim', + description: + 'Build Snowflake automation in Sim. Run SQL, ask Cortex Analyst, and manage warehouses and tasks with AI agents. Free to start.', + keywords: ['snowflake automation', 'snowflake ai agent', 'snowflake integration'], + h1: 'Snowflake Automation with Sim', + tagline: + 'Build Snowflake automation with AI agents in Sim. Run SQL, ask Cortex Analyst, sync rows, and manage warehouses and tasks.', + overview: + 'Connect Snowflake to Sim with a programmatic access token and run Snowflake automation from one AI workspace. Execute parameterized SQL; insert, update, upsert, and delete rows; load and unload staged data; browse databases, schemas, and tables; control warehouses and tasks; review query history; and call stored procedures. For example, a scheduled workflow can run Execute SQL against a revenue table, have an Agent block summarize the change since last week, and post the summary to Slack. An agent can answer a team question with Ask Cortex Analyst against a semantic view and return the generated SQL and results. A data operations workflow can check List Task Runs and rerun a failed task with Run Task.', + comparison: { + id: 'snowflake-comparison', + heading: 'Sim vs. Zapier and n8n for Snowflake', + intro: + 'Zapier and n8n both connect to Snowflake for SQL and row operations. Sim focuses on workflows where an Agent block queries Snowflake, interprets the results, and sends the insight to a tool such as Slack. None of the three receives instant events from Snowflake: Zapier polls for new rows, while Sim and n8n workflows start from a schedule or another trigger.', + columns: ['Sim', 'Zapier', 'n8n'], + rows: [ + { + label: 'Trigger model', + values: [ + { + text: 'Scheduled; no native Snowflake trigger', + }, + { + text: 'Polling for new or updated rows', + href: 'https://zapier.com/apps/snowflake/integrations', + }, + { + text: 'No Snowflake trigger node', + href: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.snowflake', + }, + ], + }, + { + label: 'AI step before acting', + values: [ + { + text: 'Agent block, built in', + }, + { + text: 'Requires a separate AI step or app', + href: 'https://zapier.com/apps/snowflake/integrations/chatgpt', + }, + { + text: 'Requires a separate AI Agent node', + href: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent', + }, + ], + }, + { + label: 'Free to start', + values: [ + { + text: 'Yes', + }, + { + text: 'Yes, 100 tasks a month on two-step Zaps', + href: 'https://zapier.com/pricing', + }, + { + text: 'Yes, when self-hosted', + href: 'https://docs.n8n.io/n8n-community-license', + }, + ], + }, + { + label: 'Natural-language questions', + values: [ + { + text: 'Ask Cortex Analyst operation', + }, + { + text: 'SQL and row actions only', + href: 'https://zapier.com/apps/snowflake/integrations', + }, + { + text: 'SQL query, insert, and update only', + href: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.snowflake', + }, + ], + }, + ], + conclusion: + 'Choose Sim when an AI agent should query Snowflake, interpret the results, and deliver the insight, rather than move rows between tools unchanged.', + }, + faqs: [ + { + question: 'Which AI agent builders integrate with Snowflake for data queries?', + answer: + 'Sim integrates with Snowflake through a block that runs parameterized SQL, lists databases, schemas, and tables, inspects schemas, and asks Cortex Analyst questions in natural language. An Agent block can choose the query, read the results, and pass them to later steps. Zapier and n8n also connect to Snowflake for SQL and row operations.', + }, + { + question: 'Can Sim query Snowflake and post insights to Slack?', + answer: + 'Yes. A Sim workflow can run Execute SQL or Ask Cortex Analyst, have an Agent block summarize the results, and send the summary with a Slack block. Start it with a Schedule trigger for recurring reports, because Sim has no trigger that fires on Snowflake data changes.', + }, + { + question: 'Does Sim support an MCP server for querying Snowflake?', + answer: + 'Sim can connect a remote MCP server that uses Streamable HTTP and give its tools to an Agent block, so a Snowflake MCP server your team runs can be added as agent tools. Sim does not ship its own Snowflake MCP server, and its built-in Snowflake block runs SQL and Cortex Analyst without one. Sim can also expose a deployed workflow, such as one that runs a Snowflake query, as a tool on an MCP server for external MCP clients.', + }, + { + question: 'How do I connect Snowflake to Sim?', + answer: + 'Create a Snowflake programmatic access token, add a Snowflake block to a workflow, and enter the token with your account details. Choose an operation such as Execute SQL or List Tables, test the workflow, and deploy it. Execute SQL runs whatever SQL the workflow passes it, so scope the token’s role to the data the workflow needs.', + }, + { + question: 'What is the difference between Sim and n8n for Snowflake?', + answer: + 'n8n’s Snowflake node runs SQL queries and inserts or updates rows, and adding AI requires a separate AI Agent node. Sim’s Snowflake block also covers upserts, staged loads and unloads, warehouses, tasks, query history, stored procedures, and Cortex Analyst, with Agent blocks built into every workflow.', + }, + ], }, outlook: { title: 'Outlook Automation with Sim', @@ -172,7 +500,7 @@ export const INTEGRATION_SEO: Record = { }, { text: 'Yes, when self-hosted', - href: 'https://docs.n8n.io/privacy-and-security/sustainable-use-license/', + href: 'https://docs.n8n.io/n8n-community-license', }, ], }, diff --git a/apps/sim/app/.well-known/api-catalog/route.ts b/apps/sim/app/.well-known/api-catalog/route.ts index c11f0ce640d..f82be29d6b1 100644 --- a/apps/sim/app/.well-known/api-catalog/route.ts +++ b/apps/sim/app/.well-known/api-catalog/route.ts @@ -8,9 +8,10 @@ const API_CATALOG_CONTENT_TYPE = 'application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"' /** - * RFC 9727 API catalog: one machine-readable entry point to the v2 REST API, linking its OpenAPI - * document (`service-desc`), reference docs (`service-doc`), and health endpoint (`status`) with - * the RFC 8631 relations. The anchor is this deployment's own origin, so a self-hosted instance + * RFC 9727 API catalog: one machine-readable entry point to the v2 REST API. The first context + * lists the API as a catalog `item` (RFC 9727 §3.1); the second links its OpenAPI document + * (`service-desc`), reference docs (`service-doc`), and health endpoint (`status`) with the + * RFC 8631 relations. Both anchors are this deployment's own origin, so a self-hosted instance * describes itself. The OpenAPI document is the one published contract; its `servers` names Sim * Cloud, so a client of another deployment uses the anchor as its base URL, as the API docs say. * `Link: rel="api-catalog"` makes a HEAD request discoverable too (RFC 9727 §2). @@ -18,12 +19,14 @@ const API_CATALOG_CONTENT_TYPE = export const GET = withRouteHandler(() => { const baseUrl = getBaseUrl() const catalogUrl = `${baseUrl}/.well-known/api-catalog` + const apiUrl = `${baseUrl}/api/v2` return NextResponse.json( { linkset: [ + { anchor: catalogUrl, item: [{ href: apiUrl }] }, { - anchor: `${baseUrl}/api/v2`, + anchor: apiUrl, 'service-desc': [ { href: `${DOCS_URL}/openapi.json`, type: 'application/vnd.oai.openapi+json' }, ], diff --git a/apps/sim/app/api/billing/update-cost/route.test.ts b/apps/sim/app/api/billing/update-cost/route.test.ts index 29f86b66aea..a0098ea6995 100644 --- a/apps/sim/app/api/billing/update-cost/route.test.ts +++ b/apps/sim/app/api/billing/update-cost/route.test.ts @@ -1,5 +1,6 @@ import { readFileSync } from 'node:fs' import { createMockRequest, resetEnvFlagsMock, setEnvFlags } from '@sim/testing' +import { createRouteContext } from '@sim/testing/helpers/http' import { billingAttributionMock, billingAttributionMockFns, @@ -9,6 +10,7 @@ import { billingPlanMock, billingPlanMockFns } from '@sim/testing/mocks/billing- import { billingUsageLogMock, billingUsageLogMockFns, + MockCumulativeUsageInFlightError, } from '@sim/testing/mocks/billing-usage-log.mock' import { billingUsageMonitorMock, @@ -18,6 +20,7 @@ import { copilotHttpMock, copilotHttpMockFns } from '@sim/testing/mocks/copilot- import { mothershipOtelMock } from '@sim/testing/mocks/mothership-otel.mock' import { sleep } from '@sim/utils/helpers' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { BillingInFlightCode } from '@/lib/mothership/generated/billing' const { mockCheckAndBillOverageThreshold, @@ -748,6 +751,42 @@ describe('POST /api/billing/update-cost — workspaceId attribution', () => { }) }) + it('answers a retryable in-flight 503 while another attempt of the request is recorded', async () => { + const billingRequestId = '0190c03f-9f7d-4b79-8b58-e7f779fd29e1' + mockRecordCumulativeUsage.mockRejectedValueOnce( + new MockCumulativeUsageInFlightError(`update-cost:${billingRequestId}`) + ) + + const res = await POST( + createMockRequest( + 'POST', + { + userId: 'user-1', + cost: 0.5, + model: 'claude-opus-4.8', + source: 'workspace-chat', + idempotencyKey: billingRequestId, + }, + { + 'x-api-key': 'internal', + 'x-sim-billing-protocol': 'direct-v1', + 'x-sim-billing-request-id': billingRequestId, + 'x-sim-billing-account-decision': 'serialized-account-decision', + } + ), + createRouteContext({}) + ) + + expect(res.status).toBe(503) + expect(res.headers.get('retry-after')).toBe('5') + await expect(res.json()).resolves.toMatchObject({ + success: false, + code: BillingInFlightCode, + retryable: true, + }) + expect(mockCheckAndBillPayerOverageThreshold).not.toHaveBeenCalled() + }) + it('retries modern settlement on a duplicate cumulative callback before returning 409', async () => { const billingRequestId = '0190c03f-9f7d-4b79-8b58-e7f779fd29e1' mockRecordCumulativeUsage diff --git a/apps/sim/app/api/billing/update-cost/route.ts b/apps/sim/app/api/billing/update-cost/route.ts index f7d2ef16fe3..6ff3f097f8e 100644 --- a/apps/sim/app/api/billing/update-cost/route.ts +++ b/apps/sim/app/api/billing/update-cost/route.ts @@ -30,6 +30,7 @@ import { import { type CumulativeUsageContextField, CumulativeUsageContextMismatchError, + CumulativeUsageInFlightError, CumulativeUsagePeriodClosedError, recordCumulativeUsage, } from '@/lib/billing/core/usage-log' @@ -43,6 +44,7 @@ import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags' import { withinDeadline } from '@/lib/core/utils/deadline' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { BillingInFlightCode } from '@/lib/mothership/generated/billing' import { BILLING_CALLBACK_OUTCOME } from '@/lib/mothership/generated/billing-protocol-v1' import { BillingRouteOutcome } from '@/lib/mothership/generated/trace-attribute-values-v1' import { TraceAttr } from '@/lib/mothership/generated/trace-attributes-v1' @@ -53,7 +55,7 @@ import { withIncomingGoSpan } from '@/lib/mothership/request/otel' const logger = createLogger('BillingUpdateCostAPI') /** * How long a cost callback waits on the payer's standing. The worker gives up on the whole - * callback after 5 s, and a cold gate read can wait on the ledger far longer; past this the + * callback after 15 s, and a cold gate read can wait on the ledger far longer; past this the * callback answers not-exceeded. The abandoned read keeps running and caches its admission, and * the next step or re-check reads a refusal again. */ @@ -82,8 +84,8 @@ function invalidBillingProtocolResponse(requestId: string, span: Span): NextResp * once it crosses the limit instead of at its next admission, with the card the worker writes * to its log. The payer is the attributed run's, or the one a direct-v1 run was admitted with. * A duplicate callback answers too: it is often a retry whose first answer was lost. An - * admission is cached per payer and actor for the gate TTL and a refusal is always re-read, so - * steady-state steps cost no ledger read. The charge is + * admission is cached per payer and actor (briefly once near the limit) and a refusal is always + * re-read, so most steps cost no ledger read. The charge is * already recorded when this runs; a gate that cannot answer reports not-exceeded and leaves the * refusal to the next step or re-check rather than ending a paying run on a database blip, * and so does a verdict read that outlasts {@link USAGE_STANDING_TIMEOUT_MS}. An exceeded @@ -510,6 +512,32 @@ async function updateCostInner(req: NextRequest, span: Span): Promise { }) }) + it('re-reads a direct-v1 account near its limit, so a continuation past it is refused', async () => { + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + try { + mockCheckUsageStatus.mockResolvedValue({ isExceeded: false, currentUsage: 4.2, limit: 5 }) + expect((await POST(request(body, directHeaders), createRouteContext({}))).status).toBe(200) + + mockCheckUsageStatus.mockResolvedValue({ isExceeded: true, currentUsage: 5.4, limit: 5 }) + /** `lru-cache` captures `performance` at load: spy its clock, then fire its 1 ms debounce. */ + const start = performance.now() + vi.spyOn(performance, 'now').mockReturnValue(start + NEAR_LIMIT_USAGE_GATE_TTL_MS + 1) + await vi.advanceTimersByTimeAsync(1) + + expect((await POST(request(body, directHeaders), createRouteContext({}))).status).toBe(402) + } finally { + vi.runOnlyPendingTimers() + vi.useRealTimers() + } + }) + it('admits a direct-v1 continuation whose usage cannot be read', async () => { mockCheckUsageStatus.mockResolvedValue({ isExceeded: true, unavailable: true }) expect((await POST(request(body, directHeaders))).status).toBe(200) diff --git a/apps/sim/app/api/copilot/chat/abort/route.test.ts b/apps/sim/app/api/copilot/chat/abort/route.test.ts index a3f9bc1f8b0..e3a516df4d7 100644 --- a/apps/sim/app/api/copilot/chat/abort/route.test.ts +++ b/apps/sim/app/api/copilot/chat/abort/route.test.ts @@ -135,6 +135,30 @@ describe('POST /api/copilot/chat/abort', () => { expect(mockAbortWorkflow).toHaveBeenCalledExactlyOnceWith('client-execution') }) + it('still cancels the run’s workflows when the abort marker cannot be written', async () => { + mockUnsettledWorkflows.mockResolvedValue(['client-execution']) + mockAbortActiveStream.mockRejectedValueOnce(new Error('redis unavailable')) + const response = await POST(abortRequest()) + expect(response.status).toBe(200) + expect(mockRequestExplicitStreamAbort).toHaveBeenCalled() + expect(mockCancelWorkflow).toHaveBeenCalledExactlyOnceWith('client-execution') + }) + + it('signals the worker before the run’s workflow ownership has been read', async () => { + let releaseRead: (ids: string[]) => void = () => {} + mockUnsettledWorkflows.mockReturnValue( + new Promise((resolve) => { + releaseRead = resolve + }) + ) + const response = POST(abortRequest()) + await vi.waitFor(() => expect(mockRequestExplicitStreamAbort).toHaveBeenCalled()) + expect(mockAbortActiveStream).toHaveBeenCalled() + releaseRead(['client-execution']) + expect((await response).status).toBe(200) + expect(mockCancelWorkflow).toHaveBeenCalledExactlyOnceWith('client-execution') + }) + it('does not signal workflows when the atomic Stop and admission closure cannot commit', async () => { mockRequestRunStop.mockRejectedValueOnce(new Error('database unavailable')) const response = await POST(abortRequest()) diff --git a/apps/sim/app/api/copilot/chat/delete/route.ts b/apps/sim/app/api/copilot/chat/delete/route.ts index 6239c4a6ba2..e4c5abaddc1 100644 --- a/apps/sim/app/api/copilot/chat/delete/route.ts +++ b/apps/sim/app/api/copilot/chat/delete/route.ts @@ -1,12 +1,10 @@ -import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { deleteCopilotChatContract } from '@/lib/api/contracts/copilot' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { deleteCopilotChat } from '@/lib/mothership/chat/deletion' import { getAccessibleCopilotChatAuth } from '@/lib/mothership/chat/lifecycle' import { chatPubSub } from '@/lib/mothership/chat-status' @@ -45,10 +43,7 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => { ) } - const [deleted] = await db - .delete(copilotChats) - .where(and(eq(copilotChats.id, parsed.chatId), eq(copilotChats.userId, session.user.id))) - .returning({ workspaceId: copilotChats.workspaceId }) + const deleted = await deleteCopilotChat({ chatId: parsed.chatId, userId: session.user.id }) if (!deleted) { return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 }) diff --git a/apps/sim/app/api/copilot/chat/queries.ts b/apps/sim/app/api/copilot/chat/queries.ts deleted file mode 100644 index ff17ad6570c..00000000000 --- a/apps/sim/app/api/copilot/chat/queries.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' -import { toError } from '@sim/utils/errors' -import { and, desc, eq, isNull } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { buildEffectiveChatTranscript } from '@/lib/mothership/chat/effective-transcript' -import { getAccessibleCopilotChat } from '@/lib/mothership/chat/lifecycle' -import { - type LiveTurnSnapshot, - readLiveTurnSnapshot, -} from '@/lib/mothership/chat/live-turn-snapshot' -import { compactToolCallParams, normalizeMessage } from '@/lib/mothership/chat/persisted-message' -import type { MothershipEffort } from '@/lib/mothership/model-options' -import { - authenticateCopilotRequestSessionOnly, - createBadRequestResponse, - createForbiddenResponse, - createInternalServerErrorResponse, - createUnauthorizedResponse, -} from '@/lib/mothership/request/http' -import { - assertActiveWorkspaceAccess, - isWorkspaceAccessDeniedError, -} from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('CopilotChatAPI') - -function transformChat(chat: { - id: string - title: string | null - model: string | null - messages: unknown - config?: unknown - effort?: MothershipEffort | null - conversationId?: string | null - resources?: unknown - createdAt: Date | null - updatedAt: Date | null -}) { - return { - id: chat.id, - title: chat.title, - model: chat.model, - messages: Array.isArray(chat.messages) ? chat.messages : [], - messageCount: Array.isArray(chat.messages) ? chat.messages.length : 0, - config: chat.config || null, - effort: chat.effort ?? null, - ...('conversationId' in chat ? { activeStreamId: chat.conversationId || null } : {}), - ...('resources' in chat - ? { resources: Array.isArray(chat.resources) ? chat.resources : [] } - : {}), - createdAt: chat.createdAt, - updatedAt: chat.updatedAt, - } -} - -type CopilotChatListRow = Pick< - typeof copilotChats.$inferSelect, - 'id' | 'title' | 'model' | 'createdAt' | 'updatedAt' -> - -function transformChatListItem(chat: CopilotChatListRow) { - return { - id: chat.id, - title: chat.title, - model: chat.model, - createdAt: chat.createdAt, - updatedAt: chat.updatedAt, - } -} - -export async function GET(req: NextRequest) { - try { - const { searchParams } = new URL(req.url) - const workflowId = searchParams.get('workflowId') - const workspaceId = searchParams.get('workspaceId') - const chatId = searchParams.get('chatId') - - const { userId: authenticatedUserId, isAuthenticated } = - await authenticateCopilotRequestSessionOnly() - if (!isAuthenticated || !authenticatedUserId) { - return createUnauthorizedResponse() - } - - if (chatId) { - const chat = await getAccessibleCopilotChat(chatId, authenticatedUserId) - if (!chat) { - return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 }) - } - - let streamSnapshot: LiveTurnSnapshot | null = null - if (chat.conversationId) { - try { - streamSnapshot = await readLiveTurnSnapshot(chat.conversationId, authenticatedUserId) - } catch (error) { - logger.warn('Failed to load copilot chat stream snapshot', { - chatId, - conversationId: chat.conversationId, - error: toError(error).message, - }) - } - } - - const normalizedMessages = Array.isArray(chat.messages) - ? chat.messages - .filter((message): message is Record => Boolean(message)) - .map(normalizeMessage) - : [] - const effectiveMessages = buildEffectiveChatTranscript({ - messages: normalizedMessages, - activeStreamId: chat.conversationId || null, - ...(streamSnapshot ? { streamSnapshot } : {}), - }).map(compactToolCallParams) - - logger.info(`Retrieved chat ${chatId}`) - return NextResponse.json({ - success: true, - chat: { - ...transformChat(chat), - messages: effectiveMessages, - ...(streamSnapshot ? { streamSnapshot } : {}), - }, - }) - } - - if (!workflowId && !workspaceId) { - return createBadRequestResponse('workflowId, workspaceId, or chatId is required') - } - - if (workspaceId) { - await assertActiveWorkspaceAccess(workspaceId, authenticatedUserId) - } - - if (workflowId) { - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId, - userId: authenticatedUserId, - action: 'read', - }) - if (!authorization.allowed) { - return createUnauthorizedResponse() - } - } - - const scopeFilter = workflowId - ? eq(copilotChats.workflowId, workflowId) - : eq(copilotChats.workspaceId, workspaceId!) - - const chats = await db - .select({ - id: copilotChats.id, - title: copilotChats.title, - model: copilotChats.model, - createdAt: copilotChats.createdAt, - updatedAt: copilotChats.updatedAt, - }) - .from(copilotChats) - .where( - and( - eq(copilotChats.userId, authenticatedUserId), - isNull(copilotChats.deletedAt), - scopeFilter - ) - ) - .orderBy(desc(copilotChats.updatedAt)) - - const scope = workflowId ? `workflow ${workflowId}` : `workspace ${workspaceId}` - logger.info(`Retrieved ${chats.length} chats for ${scope}`) - - return NextResponse.json({ - success: true, - chats: chats.map(transformChatListItem), - }) - } catch (error) { - if (isWorkspaceAccessDeniedError(error)) { - return createForbiddenResponse('Workspace access denied') - } - logger.error('Error fetching copilot chats:', error) - return createInternalServerErrorResponse('Failed to fetch chats') - } -} diff --git a/apps/sim/app/api/files/public/[token]/content/route.ts b/apps/sim/app/api/files/public/[token]/content/route.ts index d8fcd3a3d3d..b85fe6df606 100644 --- a/apps/sim/app/api/files/public/[token]/content/route.ts +++ b/apps/sim/app/api/files/public/[token]/content/route.ts @@ -5,6 +5,7 @@ import { parseRequest } from '@/lib/api/server' import { getClientIp } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { + logPublicFileError, publicFileAuthDenied, publicFileBinaryErrorResponse, publicFileShareCredential, @@ -58,7 +59,7 @@ export const GET = withRouteHandler( request.headers.get('if-none-match') ) } catch (error) { - logger.error('Error serving public shared file:', error) + logPublicFileError(logger, 'Error serving public shared file:', error) return publicFileBinaryErrorResponse(error, 'Failed to serve file') } } diff --git a/apps/sim/app/api/files/public/[token]/inline/route.ts b/apps/sim/app/api/files/public/[token]/inline/route.ts index 638eec5dda0..346fbeb88a5 100644 --- a/apps/sim/app/api/files/public/[token]/inline/route.ts +++ b/apps/sim/app/api/files/public/[token]/inline/route.ts @@ -5,6 +5,7 @@ import { parseRequest } from '@/lib/api/server' import { getClientIp } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { + logPublicFileError, publicFileAuthDenied, publicFileBinaryErrorResponse, publicFileShareCredential, @@ -48,7 +49,7 @@ export const GET = withRouteHandler( request.headers.get('if-none-match') ) } catch (error) { - logger.error('Error serving public inline image:', error) + logPublicFileError(logger, 'Error serving public inline image:', error) return publicFileBinaryErrorResponse(error, 'Failed to serve file') } } diff --git a/apps/sim/app/api/files/public/[token]/otp/route.ts b/apps/sim/app/api/files/public/[token]/otp/route.ts index 11b9fcadbcb..cd72724fb04 100644 --- a/apps/sim/app/api/files/public/[token]/otp/route.ts +++ b/apps/sim/app/api/files/public/[token]/otp/route.ts @@ -11,7 +11,11 @@ import { OTP_IP_RATE_LIMIT } from '@/lib/core/security/otp' import { afterResponse } from '@/lib/core/utils/after-response' import { getClientIp } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { publicFileAuthDenied, publicFileErrorResponse } from '@/lib/public-shares/api' +import { + logPublicFileError, + publicFileAuthDenied, + publicFileErrorResponse, +} from '@/lib/public-shares/api' import { requestPublicFileShareOtp, verifyPublicFileShareOtp, @@ -48,7 +52,7 @@ export const POST = withRouteHandler( afterResponse(accepted.deliver) return NextResponse.json({ message: 'Verification code sent' }) } catch (error) { - logger.error('Error processing OTP request:', error) + logPublicFileError(logger, 'Error processing OTP request:', error) return publicFileErrorResponse(error, 'Failed to process request') } } @@ -69,7 +73,7 @@ export const PUT = withRouteHandler( response.cookies.set(auth.cookie) return response } catch (error) { - logger.error('Error verifying OTP:', error) + logPublicFileError(logger, 'Error verifying OTP:', error) return publicFileErrorResponse(error, 'Failed to process request') } } diff --git a/apps/sim/app/api/files/public/[token]/route.ts b/apps/sim/app/api/files/public/[token]/route.ts index 2c2aed33059..66c305c4e21 100644 --- a/apps/sim/app/api/files/public/[token]/route.ts +++ b/apps/sim/app/api/files/public/[token]/route.ts @@ -9,6 +9,7 @@ import { parseRequest } from '@/lib/api/server' import { getClientIp } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { + logPublicFileError, publicFileAuthDenied, publicFileErrorResponse, publicFileShareCredential, @@ -48,7 +49,7 @@ export const GET = withRouteHandler( ownerName, }) } catch (error) { - logger.error('Error fetching public file metadata:', error) + logPublicFileError(logger, 'Error fetching public file metadata:', error) return publicFileErrorResponse(error, 'Failed to fetch file') } } @@ -70,7 +71,7 @@ export const POST = withRouteHandler( response.cookies.set(auth.cookie) return response } catch (error) { - logger.error('Error authenticating public file share:', error) + logPublicFileError(logger, 'Error authenticating public file share:', error) return publicFileErrorResponse(error, 'Failed to authenticate') } } diff --git a/apps/sim/app/api/files/public/[token]/sso/route.ts b/apps/sim/app/api/files/public/[token]/sso/route.ts index 9718d8a1e86..bd0aebe3b97 100644 --- a/apps/sim/app/api/files/public/[token]/sso/route.ts +++ b/apps/sim/app/api/files/public/[token]/sso/route.ts @@ -6,7 +6,11 @@ import { parseRequest } from '@/lib/api/server' import { RateLimiter, type TokenBucketConfig } from '@/lib/core/rate-limiter' import { getClientIp } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { publicFileAuthDenied, publicFileErrorResponse } from '@/lib/public-shares/api' +import { + logPublicFileError, + publicFileAuthDenied, + publicFileErrorResponse, +} from '@/lib/public-shares/api' import { getPublicFileShareSsoEligibility } from '@/lib/public-shares/application' export const dynamic = 'force-dynamic' @@ -47,7 +51,7 @@ export const POST = withRouteHandler( ? NextResponse.json({ eligible: result.eligible }) : publicFileAuthDenied(result) } catch (error) { - logger.error('Error checking public file SSO eligibility:', error) + logPublicFileError(logger, 'Error checking public file SSO eligibility:', error) return publicFileErrorResponse(error, 'Failed to process request') } } diff --git a/apps/sim/app/api/internal/file-doc/persist/route.ts b/apps/sim/app/api/internal/file-doc/persist/route.ts index 399791a0172..8719bb1fff5 100644 --- a/apps/sim/app/api/internal/file-doc/persist/route.ts +++ b/apps/sim/app/api/internal/file-doc/persist/route.ts @@ -3,8 +3,10 @@ import { getErrorMessage } from '@sim/utils/errors' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { persistFileDocContract } from '@/lib/api/contracts/file-doc' +import { isOffAppHost } from '@/lib/api/mcp/host-routing' import { parseRequest } from '@/lib/api/server' import { persistFileDoc } from '@/lib/collab-doc/persist' +import { MAX_COLLAB_DOC_PERSIST_BODY_BYTES } from '@/lib/collab-doc/state' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { checkInternalApiKey, createUnauthorizedResponse } from '@/lib/mothership/request/http' @@ -16,12 +18,19 @@ const logger = createLogger('FileDocPersistAPI') * the shared `x-api-key: INTERNAL_API_SECRET` secret, matching the header the realtime relay sends. * The relay owns the live doc but not the conversion engine or blob/DB access, so it ships the current * doc state here — the server-authoritative durable path that replaces the editor's client autosave. + * Excluded from the proxy matcher, so the route refuses the dedicated MCP host before auth itself. */ export const POST = withRouteHandler(async (request: NextRequest) => { + if (isOffAppHost(request)) return new NextResponse(null, { status: 404 }) const auth = checkInternalApiKey(request) if (!auth.success) return createUnauthorizedResponse() - const parsed = await parseRequest(persistFileDocContract, request, {}) + const parsed = await parseRequest( + persistFileDocContract, + request, + {}, + { maxBodyBytes: MAX_COLLAB_DOC_PERSIST_BODY_BYTES, bypassesProxy: true } + ) if (!parsed.success) return parsed.response const { workspaceId, fileId, userId, docState, expectedVersion } = parsed.data.body diff --git a/apps/sim/app/api/internal/project-file-doc/[projectId]/[fileId]/persist/route.ts b/apps/sim/app/api/internal/project-file-doc/[projectId]/[fileId]/persist/route.ts index 4699695c7ad..a81097ab5d2 100644 --- a/apps/sim/app/api/internal/project-file-doc/[projectId]/[fileId]/persist/route.ts +++ b/apps/sim/app/api/internal/project-file-doc/[projectId]/[fileId]/persist/route.ts @@ -7,12 +7,9 @@ import { internalRateLimits, } from '@/lib/api/server/routes' import { realtimeProjectFileAuth } from '@/lib/auth/realtime-file-delegation' -import { MAX_COLLAB_DOC_STATE_BYTES } from '@/lib/collab-doc/state' +import { MAX_COLLAB_DOC_PERSIST_BODY_BYTES } from '@/lib/collab-doc/state' import { persistProjectFileDoc } from '@/lib/projects/files/application/documents' -/** A base64-encoded maximum snapshot plus 1 MiB for its JSON envelope (17 MiB). */ -const PERSIST_MAX_BODY_BYTES = Math.ceil(MAX_COLLAB_DOC_STATE_BYTES / 3) * 4 + 1024 * 1024 - const persist = defineInternalJsonRoute({ contract: projectFileDocPersistContract, operation: persistProjectFileDoc.operation, @@ -27,7 +24,7 @@ const persist = defineInternalJsonRoute({ expectedVersion: body.expectedVersion, }), useCase: persistProjectFileDoc, - parseOptions: { maxBodyBytes: PERSIST_MAX_BODY_BYTES, bypassesProxy: true }, + parseOptions: { maxBodyBytes: MAX_COLLAB_DOC_PERSIST_BODY_BYTES, bypassesProxy: true }, }) /** Excluded from the proxy matcher, so the route refuses the dedicated MCP host before auth itself. */ diff --git a/apps/sim/app/api/mothership/chat/route.ts b/apps/sim/app/api/mothership/chat/route.ts index 62316bdd8f8..74f5c159b9f 100644 --- a/apps/sim/app/api/mothership/chat/route.ts +++ b/apps/sim/app/api/mothership/chat/route.ts @@ -1,14 +1,9 @@ import { type NextRequest, NextResponse } from 'next/server' -import { - mothershipChatGetQuerySchema, - mothershipChatPostEnvelopeSchema, -} from '@/lib/api/contracts/mothership-chats' -import { validationErrorResponse } from '@/lib/api/server' +import { mothershipChatPostEnvelopeSchema } from '@/lib/api/contracts/mothership-chats' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { handleUnifiedChatPost } from '@/lib/mothership/chat/post' import { validateShimEnvelope } from '@/lib/mothership/request/http' -import { GET as copilotChatGet } from '@/app/api/copilot/chat/queries' /** * Caps this response on serverless hosts only; the Node server bounds nothing with it. @@ -17,16 +12,6 @@ import { GET as copilotChatGet } from '@/app/api/copilot/chat/queries' */ export const maxDuration = 3600 -// Unified chat route surface. -export const GET = withRouteHandler((request: NextRequest) => { - const validation = mothershipChatGetQuerySchema.safeParse( - Object.fromEntries(request.nextUrl.searchParams.entries()) - ) - if (!validation.success) return validationErrorResponse(validation.error) - - return copilotChatGet(request) -}) - export const POST = withRouteHandler(async (request: NextRequest) => { const session = await getSession() if (!session?.user?.id) { diff --git a/apps/sim/app/api/mothership/chats/[chatId]/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/route.ts index 6c00c639da7..eacabc1f83d 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/route.ts @@ -11,6 +11,7 @@ import { } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { softDeleteMothershipChat } from '@/lib/mothership/chat/deletion' import { buildEffectiveChatTranscript } from '@/lib/mothership/chat/effective-transcript' import { getAccessibleCopilotChatAuth, @@ -52,7 +53,6 @@ export const GET = withRouteHandler( const chat = await getAccessibleCopilotChatWithMessages(chatId, userId, { principal, - type: 'mothership', window: fromPosition ? { kind: 'from', position: fromPosition } : window === 'latest' @@ -255,21 +255,7 @@ export const DELETE = withRouteHandler( return NextResponse.json({ success: true }) } - const [deletedChat] = await db - .update(copilotChats) - .set({ deletedAt: new Date() }) - .where( - and( - eq(copilotChats.id, chatId), - eq(copilotChats.userId, userId), - eq(copilotChats.type, 'mothership'), - isNull(copilotChats.deletedAt) - ) - ) - .returning({ - workspaceId: copilotChats.workspaceId, - organizationId: copilotChats.organizationId, - }) + const deletedChat = await softDeleteMothershipChat({ chatId, userId }) if (!deletedChat) { return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 }) diff --git a/apps/sim/app/api/mothership/memories/access/route.ts b/apps/sim/app/api/mothership/memories/access/route.ts new file mode 100644 index 00000000000..86b538b70ae --- /dev/null +++ b/apps/sim/app/api/mothership/memories/access/route.ts @@ -0,0 +1,25 @@ +import { resolveMemoryNotesAccessContract } from '@/lib/api/contracts/mothership-memories' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, +} from '@/lib/api/server/routes' +import { internalCopilotAuth } from '@/lib/mothership/auth/internal' +import { + MEMORY_NOTES_ACCESS_AUDIENCE, + resolveMemoryNotesAccess, + resolveMemoryNotesAccessOperation, +} from '@/lib/mothership/memories/application/resolve-access' + +export const POST = defineInternalJsonRoute({ + contract: resolveMemoryNotesAccessContract, + auth: internalCopilotAuth(MEMORY_NOTES_ACCESS_AUDIENCE, { organization: true }), + operation: resolveMemoryNotesAccessOperation, + rateLimit: internalRateLimits.none({ + reason: + 'Private worker preflight rechecks live memory authority without reading or writing note content.', + }), + errorPolicy: internalOrchestrationErrorPolicy, + mapInput: ({ body }) => body, + useCase: resolveMemoryNotesAccess, +}) diff --git a/apps/sim/app/api/mothership/runs/control/route.test.ts b/apps/sim/app/api/mothership/runs/control/route.test.ts index b6d5151ce9d..a5513010fea 100644 --- a/apps/sim/app/api/mothership/runs/control/route.test.ts +++ b/apps/sim/app/api/mothership/runs/control/route.test.ts @@ -15,6 +15,7 @@ import { workspaceContextMockFns, } from '@sim/testing/mocks/workspace-context.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { OrchestrationError } from '@/lib/core/orchestration/types' vi.unmock('@/lib/mothership/request/http') vi.mock('@/lib/auth/ban', () => authBanMock) @@ -73,17 +74,44 @@ describe('worker run control boundary', () => { expect(mocks.run).not.toHaveBeenCalled() expect(mocks.stopped).not.toHaveBeenCalled() }) - it('conceals a chat belonging to another actor', async () => { - expect((await POST(request({ 'x-mothership-user-id': 'other-actor' }))).status).toBe(404) + it("answers Stop for a chat that is not the actor's, without reading its state", async () => { + const response = await POST(request({ 'x-mothership-user-id': 'other-actor' })) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ stopped: true }) expect(mocks.stopped).not.toHaveBeenCalled() }) + it('answers Stop once the chat is deleted', async () => { + resetDbChainMock() + queueTableRows(schemaMock.copilotChats, []) + const response = await POST(request()) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ stopped: true }) + expect(mocks.stopped).not.toHaveBeenCalled() + }) + it('answers Stop once the actor lost workspace access', async () => { + mocks.authorize.mockRejectedValue( + new OrchestrationError('forbidden', 'Insufficient workspace permissions') + ) + const response = await POST(request()) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ stopped: true }) + expect(mocks.stopped).not.toHaveBeenCalled() + }) + it('lets a live run continue', async () => { + mocks.stopped.mockResolvedValue(false) + const response = await POST(request()) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ stopped: false }) + }) it.each([ null, { chatId: 'other-chat', workspaceId: 'workspace' }, { chatId, workspaceId: 'other-workspace' }, - ])('refuses a missing or mismatched canonical run: %j', async (run) => { + ])('answers Stop for a missing or mismatched canonical run: %j', async (run) => { mocks.run.mockResolvedValue(run) - expect((await POST(request())).status).toBe(404) + const response = await POST(request()) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ stopped: true }) expect(mocks.stopped).not.toHaveBeenCalled() }) it('does not turn a database outage into permission to continue', async () => { diff --git a/apps/sim/app/api/organizations/[id]/transfer-ownership/route.ts b/apps/sim/app/api/organizations/[id]/transfer-ownership/route.ts index 13f61580a3f..53ef7dab812 100644 --- a/apps/sim/app/api/organizations/[id]/transfer-ownership/route.ts +++ b/apps/sim/app/api/organizations/[id]/transfer-ownership/route.ts @@ -13,6 +13,7 @@ import { transferOrganizationOwnership, } from '@/lib/billing/organizations/membership' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' const logger = createLogger('TransferOwnershipAPI') @@ -150,6 +151,7 @@ export const POST = withRouteHandler( memberId: selfMember.id, spareSessionToken: session.session.token, }) + deliverRunStops(removeResult.stoppedRuns) if (!removeResult.success) { logger.error('Transfer succeeded but self-removal failed', { diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts index 76637262c6f..759f1b3bb6b 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts @@ -43,6 +43,7 @@ import { } from '@/lib/billing/organizations/membership' import { isBillingEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { adminBadRequestResponse, @@ -271,6 +272,7 @@ export const DELETE = withRouteHandler( memberId, skipBillingLogic, }) + deliverRunStops(result.stoppedRuns) if (!result.success) { if (result.error === 'Cannot remove organization owner') { diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts index 3536ff44dfb..44c86c2128b 100644 --- a/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts @@ -33,6 +33,7 @@ import { } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { ProjectConflictError } from '@/lib/projects/errors' import { revokeWorkspaceAccessTx } from '@/lib/workspaces/access/workspace-access' import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' @@ -281,12 +282,13 @@ export const DELETE = withRouteHandler( expectedPermissionId: existingMember.id, }) if (!result.revoked) { - if (result.reason === 'membership-changed') return false + if (result.reason === 'membership-changed') return null throw new WorkspaceBillingAccountRemovalError() } - return true + return result }) if (!removed) return adminNotFoundResponse('Workspace member') + deliverRunStops(removed.stoppedRuns) logger.info(`Admin API: Removed member ${memberId} from workspace ${workspaceId}`, { userId: existingMember.userId, diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts index 4b8206b4fe4..83e40e62613 100644 --- a/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts +++ b/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts @@ -50,6 +50,7 @@ import { import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { ProjectConflictError } from '@/lib/projects/errors' import { revokeWorkspaceAccessTx } from '@/lib/workspaces/access/workspace-access' import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' @@ -397,12 +398,13 @@ export const DELETE = withRouteHandler( expectedPermissionId: existingPermission.id, }) if (!result.revoked) { - if (result.reason === 'membership-changed') return false + if (result.reason === 'membership-changed') return null throw new WorkspaceBillingAccountRemovalError() } - return true + return result }) if (!removed) return adminNotFoundResponse('Workspace member') + deliverRunStops(removed.stoppedRuns) logger.info(`Admin API: Removed user ${userId} from workspace ${workspaceId}`) diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts index c00d5831e10..0c3eeb6a67b 100644 --- a/apps/sim/app/api/workspaces/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/route.ts @@ -17,6 +17,7 @@ const logger = createLogger('WorkspaceByIdAPI') import { db } from '@sim/db' import { permissions, workspace } from '@sim/db/schema' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { getEffectiveWorkspacePermission, getUserEntityPermissions, @@ -284,6 +285,7 @@ export const DELETE = withRouteHandler( const archiveResult = await archiveWorkspace(workspaceId, { requestId: `workspace-${workspaceId}`, + deliverStops: deliverRunStops, }) /** Recorded first: a retry that finishes an earlier partial deletion still archives it. */ diff --git a/apps/sim/app/markdown-not-found/route.ts b/apps/sim/app/markdown-not-found/route.ts new file mode 100644 index 00000000000..666f7a81810 --- /dev/null +++ b/apps/sim/app/markdown-not-found/route.ts @@ -0,0 +1,38 @@ +import { toSiteUrl } from '@/lib/core/utils/urls' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { DOCS_URL } from '@/lib/help-links' + +const NOT_FOUND_MARKDOWN = `# Page not found + +There is no page at this address. It may have moved or never existed. + +- [Sim home](${toSiteUrl('/')}) +- [Documentation](${DOCS_URL}) +- [llms.txt](${toSiteUrl('/llms.txt')}): an index of every public page +- [Sitemap](${toSiteUrl('/sitemap.xml')}) +` + +/** + * The 404 body for a client that asks for Markdown. `next.config.ts` rewrites an unmatched path + * here, whatever its method, only when its `Accept` header names `text/markdown`, so browsers keep + * the HTML `not-found` page. Uncached like that page, and `Vary: Accept` because the same URL + * answers both ways. + */ +const notFound = withRouteHandler( + () => + new Response(NOT_FOUND_MARKDOWN, { + status: 404, + headers: { + 'Content-Type': 'text/markdown; charset=utf-8', + 'Cache-Control': 'no-store', + Vary: 'Accept', + }, + }) +) + +export const GET = notFound +export const POST = notFound +export const PUT = notFound +export const PATCH = notFound +export const DELETE = notFound +export const OPTIONS = notFound diff --git a/apps/sim/content/changelog/edition-2026-10-15/production.md b/apps/sim/content/changelog/edition-2026-10-15/production.md new file mode 100644 index 00000000000..7667b3b6063 --- /dev/null +++ b/apps/sim/content/changelog/edition-2026-10-15/production.md @@ -0,0 +1,61 @@ +# Oracle Database, Checkr, and Shopify updates + +Edition: `2026-10-15`. This is the existing scheduled edition, expanded during human editorial review. + +Article: `../oracle-checkr-and-shopify/index.mdx` + +Canonical: https://www.sim.ai/changelog/oracle-checkr-and-shopify + +Publication: pending the normal site release and hosted verification. A staging deployment is not production publication. + +## Selection and release evidence + +The initial draft considered only an older candidate batch and focused on Oracle Database. This catch-up edition adds substantial integration changes that were missing from earlier posts, followed by two concrete Chat fixes from the latest release. It does not imply that every item shipped on the article's publication date. + +| Source | Verified release | Coverage | +| --- | --- | --- | +| [Oracle Database](https://github.com/simstudioai/sim/pull/7378) | [v0.9.17](https://github.com/simstudioai/sim/releases/tag/v0.9.17) | Queries, structured writes, schema inspection, reusable connections, encrypted transport, and access requirements. | +| [Checkr](https://github.com/simstudioai/sim/pull/8824) | v0.9.17 | Candidates, invitations, reports, and report/invitation event triggers. | +| [Shopify](https://github.com/simstudioai/sim/pull/8833) | v0.9.17 | Bulk product variants, fulfillment orders, shipment tracking, and refresh of expiring connections. | +| [Ramp and Vanta](https://github.com/simstudioai/sim/pull/8802) | v0.9.17 | Saved client-credentials connections selected from blocks. | +| [Chat recovery](https://github.com/simstudioai/sim/pull/9059) | [v0.9.23](https://github.com/simstudioai/sim/releases/tag/v0.9.23) | Recovery of interrupted runs after a server restart without reopening the chat. No exactly-once execution claim. | +| [Chat panel resizing](https://github.com/simstudioai/sim/pull/9065) | v0.9.23 | Retains the selected width when focus changes during a resize. | + +Release membership comes from explicit PR references in the published release notes, not merge dates. Oracle and the integration notes belong to v0.9.17; the Chat section is explicitly v0.9.23. The combined edition uses its canonical URL as its RSS identity, without a single `release.url`. + +## Availability and factual review + +- The deployed public block catalog exposes Oracle Database, Checkr, Shopify, Ramp, and Vanta without preview gating. Their public integration pages and guides resolve successfully. +- Oracle Database requires version 12.1 or later and network access from the Sim runtime. Self-hosted installations require Node.js 24. Connection and SQL details remain in the linked guide. +- Checkr requires the user's API key. Ramp and Vanta require their client ID and secret. Existing Vanta workflows with inline credentials must create and select a saved credential before running again; the article links the setup guide. The copy makes no claims about provider execution, connection success, or account permissions established by the image. +- The latest release's Project-file work and workflow tests remain gated or foundational; they are excluded. OCI, Fusion, and EPM infrastructure has no corresponding public integration block to announce here. +- Routine dependency work, internal refactors, and the original narrow workflow-lint note remain in technical release history. + +## Media provenance + +- Asset: `/changelog/oracle-operations-v1.jpg`, a 496 × 226 JPEG. +- Captured October 15, 2026 from the actual Oracle Database block in a disposable local demo workspace, running this branch on the current staging base. +- Opened the real Operation dropdown and widened the editor panel before capture. The complete rounded menu shows search and all six operations. The optimized crop excludes the editor's outer edge and unrelated workspace content. +- No generated UI, fake credentials, edited labels, or fabricated query results. This capture demonstrates available operations; it is not an execution demonstration. Provider-account execution was not part of this capture. +- The article retains the shared 12px radius, width limit, and existing image lightbox. One focused image serves the edition; supporting notes do not need separate media. +- Full capture and review evidence are retained privately. Do not publish environment identifiers or logs. + +## Editorial and delivery checklist + +- [x] Concise lead plus five useful one-sentence notes within the body word budget. +- [x] Each supporting change traced to its actual release; no repeated coverage from earlier articles. +- [x] Public integration catalogs and contextual Oracle setup guide verified. +- [x] Authentic media captured with complete menu edges; access restrictions retained. +- [x] Desktop and 390px mobile review, including dark-theme icons and image-viewer zoom, dismissal, and focus return. +- [x] Generated artifacts, lint, type-check, 60 audits, block-registry checks, and HTTP discovery checks pass locally. Repository test suites run in CI. +- [ ] Both current-head PR reviews and final CI checks complete. +- [ ] Staging article, media, discovery feeds, and links verified after deployment. +- [ ] Production article and media verified before publication records or distribution advance. + +## Announcement drafts + +**Social:** Oracle Database and Checkr join Sim's integrations. Shopify adds bulk variant and fulfillment actions, while Ramp and Vanta connections can be reused across workflows. This changelog also covers the latest Chat recovery and resizing fixes. + +**Email:** Query and update Oracle Database from your agents, run Checkr background-check workflows, and manage more Shopify operations. Saved Ramp and Vanta connections reduce repeated setup. Read the changelog for requirements and the latest Chat fixes. + +These are drafts for the final canonical article. Distribution follows production verification. diff --git a/apps/sim/content/changelog/editorial-standard.md b/apps/sim/content/changelog/editorial-standard.md index 1a41a3d4b55..e26b6e688e2 100644 --- a/apps/sim/content/changelog/editorial-standard.md +++ b/apps/sim/content/changelog/editorial-standard.md @@ -15,10 +15,10 @@ Keep existing article URLs, original dates, and RSS identities when refining his - Headline: name the capability or action, at most 65 characters. Avoid generic headlines such as “A better experience.” - Summary: 15–40 words for search, sharing, and RSS, naming Sim and explaining the capability in plain language. It must make sense without the image. - Body: say what changed and explain the behavior a reader needs to understand it. There is no target length or minimum; keep the body under 250 words. Do not expand a short announcement with generic use cases, repeated benefits, or a setup tutorial. Name specific capabilities or controls only when they add information. -- Release coverage: inspect the technical release for other meaningful user-facing changes. Include up to three concise notes under “Also in vX.Y.Z” when source evidence establishes that they belong to the same release. Group them sensibly, avoid repeating notes across articles from that release, and link to the relevant guide or product page. A focused feature announcement can remain short. Do not infer release membership from merge timing or add unrelated changes just to make an entry longer. +- Release coverage: inspect the technical release for other meaningful user-facing changes. Include up to three concise notes under “Also in vX.Y.Z” when source evidence establishes that they belong to the same release. Group them sensibly, avoid repeating notes across articles from that release, and link to the relevant guide or product page. A manually curated catch-up edition can cover multiple releases when each section names its verified version; allow up to five notes total within the same word budget. A focused feature announcement can remain short. Do not infer release membership from merge timing or add unrelated changes just to make an entry longer. - Detail: every sentence must contribute a product change, useful behavioral detail, or necessary access information. Examples are optional and earn their place only when they make an unfamiliar change easier to understand. Leave full setup instructions and model inventories to the linked docs and catalogs. Never invent customer results or benchmarks. - Media: place the reviewed image or video immediately after the article header. Both use the shared lightbox; videos show a plain poster with playback, seeking, and zoom controls inside the viewer. Use concise alt text or an accessible action label without a visible video description. Do not add captions or prose about how the capture was made, whether a query ran, sample credentials, or what a screenshot does not prove. Keep capture provenance and review evidence in production.md. -- Supporting notes: one sentence per change, up to three per article. Omit empty sections and technical maintenance with no meaningful user impact. Keep each note’s source and release evidence in production.md. The lead image need not illustrate every supporting note. +- Supporting notes: one sentence per change, up to three for a focused announcement or five for a manually reviewed catch-up edition. Omit empty sections and technical maintenance with no meaningful user impact. Keep each note’s source and release evidence in production.md. The lead image need not illustrate every supporting note. - Links: one descriptive setup/documentation link, plus an integration, model, or product link when directly relevant. Use the actual catalog slug and guide. Put links in context; avoid a wall of generic “Learn more” links. - Availability: keep restrictions that affect whether someone can use the feature, such as a required plan or administrator role, in one concise sentence. Link to documentation for setup details. Keep unverified rollout questions, source analysis, capture instructions, and approval checkboxes in production.md. Never substitute a capture disclaimer for verifying a claim. @@ -38,7 +38,7 @@ Every production brief records: Use the same clean demo workspace and product theme. Frame the relevant panel with enough context to locate it, and make the result legible at a 390px viewport. Hide unrelated private sidebar content before recording. Keep UI text and controls intact; never generate a different product state to improve a screenshot. Prefer recording at the intended framing over aggressive crops later. -Complete sign-in and credential setup before capturing. Never save a placeholder key and present it as a working connection, or treat a saved key as evidence that the provider accepted it. A model-picker image can show model selection without a connected account; a demonstration of execution must use a working credential and a real result. Keep these production checks out of public copy. +Complete sign-in and credential setup before capturing. Never save a placeholder key and present it as a working connection, or treat a saved key as evidence that the provider accepted it. A model picker or operation menu can show available choices without a connected account; a demonstration of execution must use a working credential and a real result. Keep these production checks out of public copy. Begin with the task ready to perform, show the action, and hold on its result. Avoid decorative intros, music, title cards, and excessive cursor movement. Keep the headline and important explanation in HTML. Record videos assembled from still screenshots as edited walkthroughs in the production brief; do not describe them as continuous recordings in public copy. diff --git a/apps/sim/content/changelog/maintenance-workflow.md b/apps/sim/content/changelog/maintenance-workflow.md index 5c937f86047..8360fe1f6e7 100644 --- a/apps/sim/content/changelog/maintenance-workflow.md +++ b/apps/sim/content/changelog/maintenance-workflow.md @@ -52,7 +52,7 @@ The GitHub tool supports pagination parameters, but the current visual List pull Use the API block for the final `POST /repos/{owner}/{repo}/pulls` with fixed repository, head/base branches, and boolean `draft: true`. The visual Create as Draft dropdown stores string values, while the GitHub endpoint expects a boolean; the explicit JSON request avoids depending on implicit coercion. Verify the returned head, base, and draft fields during acceptance. -Set finite page, payload, and model-input budgets. For example, stop a scan after 20 GitHub pages and stop a model batch after 20 candidate summaries. Fetch relevant file excerpts only for shortlisted candidates, rather than sending every patch to a model. Treat reaching a cap as an incomplete scan: preserve saved candidates, keep the last completed boundary, and surface a catch-up action. Split an oversized interval or deliberately raise the reviewed budget; do not repeat an impossible capped scan forever or silently advance past it. +Set finite page, payload, and model-input budgets. For example, stop a scan after 20 GitHub pages and stop a model batch after 20 candidate summaries. For editorial review, combine the ten newest eligible candidates with the ten oldest eligible backlog candidates and deduplicate by row identity before requesting evidence. This keeps recent releases visible while older unreviewed features continue moving through review. The resulting batch stays at or below twenty. Fetch relevant file excerpts only for shortlisted candidates, rather than sending every patch to a model. Treat reaching a cap as an incomplete scan: preserve saved candidates, keep the last completed boundary, and surface a catch-up action. Split an oversized interval or deliberately raise the reviewed budget; do not repeat an impossible capped scan forever or silently advance past it. ## Drafting contract diff --git a/apps/sim/content/changelog/oracle-checkr-and-shopify/index.mdx b/apps/sim/content/changelog/oracle-checkr-and-shopify/index.mdx new file mode 100644 index 00000000000..41a64f8b0d0 --- /dev/null +++ b/apps/sim/content/changelog/oracle-checkr-and-shopify/index.mdx @@ -0,0 +1,40 @@ +--- +slug: oracle-checkr-and-shopify +title: 'Oracle Database, Checkr, and Shopify updates' +description: 'Connect Sim agents to Oracle Database and Checkr, manage more Shopify operations, and reuse saved Ramp and Vanta connections across workflows.' +date: '2026-10-15T17:00:00Z' +authors: [waleed] +tags: [Integrations, Workflows] +ogImage: /changelog/oracle-operations-v1.jpg +ogAlt: 'Oracle Database operations in the Sim workflow editor' +technical: false +draft: false +release: + versions: [v0.9.17, v0.9.23] +--- + +
+ +
+ +[Oracle Database](/integrations/oracle-database) joins Sim's integrations. Query records, insert or update rows, and inspect tables and columns from a workflow. Save a connection in Integrations and reuse it across your agents, with support for encrypted connections. + +Requires Oracle Database 12.1 or later, reachable from your Sim runtime; self-hosted installations need Node.js 24. [Connect Oracle Database](https://docs.sim.ai/integrations/oracledb). + +### More integrations in v0.9.17 + +- **[Checkr](/integrations/checkr):** Create candidates, send background-check invitations, read reports, and start workflows from report or invitation events using your Checkr API key. +- **[Shopify](/integrations/shopify):** Create or update product variants in bulk, look up fulfillment orders, and update shipment tracking; expiring connections refresh automatically. +- **[Ramp](/integrations/ramp) and [Vanta](/integrations/vanta):** Save a client ID and secret once and reuse the connection across blocks. Existing workflows with inline Vanta credentials must [create and select a saved credential](https://docs.sim.ai/integrations/vanta) before running again. + +### Chat fixes in v0.9.23 + +- Interrupted Chat runs can recover after a server restart without waiting for you to reopen the chat. +- Resizing a Chat panel keeps your chosen width even if you switch windows before releasing the pointer. + +See the full technical notes for [v0.9.17](https://github.com/simstudioai/sim/releases/tag/v0.9.17) and [v0.9.23](https://github.com/simstudioai/sim/releases/tag/v0.9.23). diff --git a/apps/sim/content/library/best-ai-agent-platforms-for-enterprise-teams-2026/index.mdx b/apps/sim/content/library/best-ai-agent-platforms-for-enterprise-teams-2026/index.mdx index 3d87dcdc117..74e3b504619 100644 --- a/apps/sim/content/library/best-ai-agent-platforms-for-enterprise-teams-2026/index.mdx +++ b/apps/sim/content/library/best-ai-agent-platforms-for-enterprise-teams-2026/index.mdx @@ -3,16 +3,16 @@ slug: best-ai-agent-platforms-for-enterprise-teams-2026 title: 'Best AI Agent Platforms for Enterprise Teams in 2026: SSO, Audit Logs, and Governance' description: 'Compare enterprise AI agent platforms in 2026 on SSO, SCIM, role-based access, audit logs, human approval, self-hosting, and licensing, with a procurement scorecard.' date: 2026-08-09 -updated: 2026-10-01 +updated: 2026-10-16 authors: - andrew -readingTime: 13 +readingTime: 24 tags: [AI Agents, Enterprise AI, Governance, Security, Sim] ogImage: /library/best-ai-agent-platforms-for-enterprise-teams-2026/cover.jpg draft: false faq: - q: "What is the best enterprise AI agent platform?" - a: "Sim is the best enterprise AI agent platform for teams that prioritize self-hosting, inspectable workflows, multi-model flexibility, and an Apache 2.0 open-source core; ecosystem-specific enterprises may prefer Microsoft Copilot Studio, Google Vertex AI Agent Builder, Amazon Bedrock Agents, or Salesforce Agentforce." + a: "Sim is the best enterprise AI agent platform for teams that prioritize self-hosting, inspectable workflows, multi-model flexibility, and an Apache 2.0 open-source core, while apps/sim/ee uses the separate Sim Enterprise License and requires an Enterprise subscription for production use; ecosystem-specific enterprises may prefer Microsoft Copilot Studio, Google Vertex AI Agent Builder, Amazon Bedrock Agents, or Salesforce Agentforce." - q: "Which AI agent platform is best for enterprise governance?" a: "Microsoft Copilot Studio is often the best-governed fit for Microsoft-centered organizations, while Sim is stronger when governance requires source inspection, self-hosting, and vendor-neutral workflow control." - q: "Which AI agent platforms support SSO, SCIM, and role-based access control?" @@ -20,17 +20,17 @@ faq: - q: "Does Sim have audit logs?" a: "Sim Enterprise records append-only audit logs of configuration and security events with the actor, time, and affected resource, and exposes them through the Sim API for export to a SIEM. Workflow execution logs separately trace each run block by block." - q: "Which AI agent platform can be self-hosted?" - a: "Sim and n8n can be self-hosted, but Sim's core uses the OSI-approved Apache License 2.0 while n8n uses the source-available Sustainable Use License." + a: "Sim and n8n can be self-hosted, but Sim's core uses the OSI-approved Apache License 2.0 while apps/sim/ee uses the separate Sim Enterprise License and n8n uses the source-available Sustainable Use License." - q: "Is Sim open source?" a: "Sim's core is open source under the Apache License 2.0, an OSI-approved license that permits commercial use, modification, and self-hosting subject to the license terms; enterprise features in apps/sim/ee use the separate Sim Enterprise License, which requires an Enterprise subscription for production use." - q: "Is n8n open source?" - a: "n8n is source-available under the Sustainable Use License as of September 2026, but that license is not OSI-approved and includes restrictions beyond a conventional open-source license." + a: "n8n is source-available under the Sustainable Use License as of October 2026, but that license is not OSI-approved and includes restrictions beyond a conventional open-source license." - q: "What is the best n8n alternative for enterprise teams?" - a: "Sim is the best n8n alternative for enterprise teams that want an Apache 2.0-licensed core, self-hosting, visual AI workflows, and model-provider flexibility." + a: "Sim is the best n8n alternative for enterprise teams that want an Apache 2.0-licensed core, self-hosting, visual AI workflows, and model-provider flexibility, while apps/sim/ee remains governed by the separate Sim Enterprise License." - q: "What is the difference between Sim and n8n?" - a: "Sim is an AI agent workflow platform with an Apache 2.0 core, focused on visual multi-model orchestration, while n8n is a broader workflow automation platform distributed under a source-available Sustainable Use License." + a: "Sim is the open-source AI workspace with an Apache 2.0 core and separately licensed apps/sim/ee enterprise code, focused on visual multi-model orchestration, while n8n is a broader workflow automation platform distributed under a source-available Sustainable Use License." - q: "What is the difference between Sim and Gumloop?" - a: "Sim emphasizes an Apache 2.0 core, self-hosting, and portable multi-model workflows, while Gumloop is a proprietary hosted automation product whose current deployment and plan capabilities should be confirmed directly with Gumloop." + a: "Sim emphasizes an Apache 2.0 core, separately licensed apps/sim/ee enterprise code, self-hosting, and portable multi-model workflows, while Gumloop is a proprietary hosted automation product whose current deployment and plan capabilities should be confirmed directly with Gumloop." - q: "Is Sim free?" a: "Sim's core can be self-hosted under the Apache License 2.0 without a software license fee, while production use of enterprise features in apps/sim/ee requires an Enterprise subscription, and hosted Sim plans and infrastructure usage are governed by the current Sim pricing terms." - q: "Which AI agent platform is best for Microsoft 365?" @@ -53,19 +53,61 @@ faq: a: "Enterprise AI agent platform cost depends on each vendor’s billing unit plus model inference, storage, retrieval, networking, integrations, observability, support, and the engineering effort required to operate the system." - q: "What is the safest enterprise AI agent platform?" a: "The safest enterprise AI agent platform is the platform that fits the enterprise’s approved trust boundary and enforces least privilege, explicit approvals, complete audit records, controlled model access, and tested failure handling for the deployed workflow." + - q: "Which AI agent platforms are SOC 2 compliant?" + a: "An AI agent platform is SOC 2 examined only when the vendor can provide a current auditor’s report covering the product and deployment mode under review. Buyers should confirm the report type, period, scope, criteria, carve-outs, and exceptions rather than relying on a logo." + - q: "Is SOC 2 Type II better than SOC 2 Type I for evaluating an AI agent platform?" + a: "SOC 2 Type II gives enterprise buyers more operating evidence because it evaluates whether scoped controls operated over a period, while Type I addresses control design at a point in time. Neither report proves that every product, deployment, or security risk is covered." + - q: "Which AI agent platform supports data residency?" + a: "An AI agent platform supports a residency requirement only when its vendor contractually identifies where application data, logs, backups, support data, and subprocessors operate. A regional hosting choice alone does not establish end-to-end residency." + - q: "Does self-hosting guarantee GDPR compliance?" + a: "Self-hosting Sim, n8n, or another AI agent platform does not guarantee GDPR compliance because the customer must still govern lawful processing, model endpoints, integrations, subprocessors, access, retention, deletion, and international transfers." + - q: "Can Sim run on-premises or in a private VPC?" + a: "Sim can be self-hosted in customer-controlled infrastructure, but buyers should validate the exact network, storage, model, telemetry, availability, and support architecture required for their on-premises or private-VPC definition." + - q: "Can Sim use local AI models when self-hosted?" + a: "Sim can connect a self-hosted deployment to local models through Ollama, vLLM, LM Studio, or LiteLLM without requiring Enterprise solely for local-model access." + - q: "Is Sim or n8n better for enterprise teams?" + a: "Sim is the stronger choice in this comparison for enterprises prioritizing an Apache 2.0 core, AI-agent development, self-hosted local-model support, and separately licensed enterprise governance options, while n8n remains a strong incumbent for teams centered on general workflow automation and its existing integration ecosystem." + - q: "What is the best n8n alternative for enterprise AI agents?" + a: "Sim is the best n8n alternative in this comparison for enterprise teams that want the open-source AI workspace with an Apache 2.0 core, self-hosting, local-model support, and enterprise governance capabilities separately governed by the Sim Enterprise License." + - q: "What is the best AI agent platform overall?" + a: "Sim ranks first for enterprise teams in this comparison, while Best AI Agent Platforms and Builders in 2026 covers the broader best AI agent platform question across enterprise and non-enterprise use cases." + - q: "Which AI agent platform has version control and rollback?" + a: "An AI agent platform has enterprise-ready version control and rollback only when it can identify complete releases, promote tested versions, restore a known-good production configuration, address in-flight runs, and record the change in an audit trail." + - q: "Is workflow edit history the same as rollback?" + a: "Workflow edit history is not the same as rollback because an enterprise rollback must restore the complete deployable configuration and define what happens to active executions, credentials, dependencies, and environment-specific settings." + - q: "How do enterprises set budget caps for AI agents?" + a: "Enterprise teams should combine platform controls with model-provider quotas, model restrictions, rate limits, concurrency limits, approval gates, and automated shutdown rules when a platform does not offer an enforceable monetary cap." + - q: "Is a cost alert the same as a budget cap?" + a: "A cost alert is not a budget cap because an alert reports that a threshold has been approached or crossed, while a cap constrains or stops additional usage according to a defined enforcement policy." + - q: "Does BYOK reduce AI agent costs?" + a: "Sim BYOK lets teams use their own provider credentials and can improve direct provider billing and attribution, but actual savings depend on provider prices, model selection, workload design, and the platform costs that remain." + - q: "How should enterprises allocate AI agent costs across teams?" + a: "An enterprise AI agent platform should attribute costs by organization, workspace, business unit, team, workflow, model, provider, environment, and time period so finance and engineering can identify both owners and cost drivers." + - q: "How do enterprises govern AI agents built by different teams?" + a: "Enterprise teams should govern AI agents with central identity, policy, audit, credential, model, and reporting controls while delegating bounded administration to individual business units and workspaces." + - q: "How do SSO, SCIM, RBAC, and audit logs work together?" + a: "SSO authenticates enterprise users, SCIM manages their lifecycle, RBAC limits what they can do, and audit logs record governed actions; an enterprise AI agent platform needs all four controls to support centralized access governance." + - q: "What should an AI agent audit log contain?" + a: "An AI agent audit log should identify the actor, action, affected resource, timestamp, outcome, and relevant administrative context for authentication, membership, permissions, credentials, workflow changes, deployments, and policy changes." + - q: "Are execution traces the same as audit logs?" + a: "Execution traces are not the same as audit logs because traces explain an agent run’s technical behavior, while audit logs establish which human or service identity performed a governed administrative action." + - q: "What should procurement ask an AI agent platform vendor?" + a: "Procurement should ask every AI agent platform vendor for current security reports, scope documentation, contracts, subprocessors, data flows, deployment architecture, recovery procedures, access controls, audit specifications, pricing terms, service commitments, and license terms." + - q: "How often should enterprises review AI agent platform compliance evidence?" + a: "Enterprise teams should review AI agent platform evidence at least at every vendor assessment cycle and whenever the vendor changes its architecture, subprocessors, deployment model, audit scope, license, pricing, or contractual commitments." --- ## TL;DR -Sim is the best enterprise AI agent platform for teams that prioritize inspectable workflows, self-hosting, model choice, and an Apache 2.0 open-source core. Microsoft Copilot Studio, Google Vertex AI Agent Builder, Amazon Bedrock Agents, Salesforce Agentforce, n8n, and IBM watsonx Orchestrate can be stronger fits for enterprises already standardized on their respective ecosystems. +Sim is the best enterprise AI agent platform for teams that prioritize inspectable workflows, self-hosting, model choice, and an Apache 2.0 open-source core, while `apps/sim/ee` is governed by the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE) and requires an active Enterprise subscription for production use. Microsoft Copilot Studio, Google Vertex AI Agent Builder, Amazon Bedrock Agents, Salesforce Agentforce, n8n, and IBM watsonx Orchestrate can be stronger fits for enterprises already standardized on their respective ecosystems. Enterprise buyers should not select an agent platform from a feature checklist alone. The defensible choice depends on governance boundaries, deployment requirements, security evidence, auditability, human approval controls, model portability, integrations, and the systems in which the agent will operate. -This guide compares enterprise AI agent platforms as of September 2026. Product capabilities, packaging, and billing can change, so procurement teams should confirm contract-specific details with each vendor before purchase. +This guide compares enterprise AI agent platforms as of October 2026. Product capabilities, packaging, and billing can change, so procurement teams should confirm contract-specific details with each vendor before purchase. ## What is the best enterprise AI agent platform in 2026? -Sim is the best enterprise AI agent platform in 2026 for organizations that want [visual agent workflows, model flexibility, self-hosting, and an inspectable core under the Apache License 2.0](https://github.com/simstudioai/sim). +Sim is the best enterprise AI agent platform in 2026 for organizations that want visual agent workflows, model flexibility, self-hosting, and an inspectable [core under the Apache License 2.0](https://github.com/simstudioai/sim), while `apps/sim/ee` is governed by the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). The best choice changes when an enterprise has a stronger ecosystem constraint: @@ -82,29 +124,168 @@ This guide focuses on what enterprise security, IT, and procurement teams must v Sim, n8n, Microsoft Copilot Studio, Google Vertex AI Agent Builder, Amazon Bedrock Agents, Salesforce Agentforce, and IBM watsonx Orchestrate differ most in deployment control, governance model, approval design, ecosystem reach, and commercial structure. -| Platform | Governance and security review | Deployment | Auditability and human approval | Model support | Integrations | Best enterprise use case | +| Platform | Governance and security review | Compliance, privacy, residency, and deployment | Identity, audit, and approvals | Version control and rollback | Cost and business-unit governance | Best enterprise use case | |---|---|---|---|---|---|---| -| Sim | [Inspectable source with an Apache 2.0 core](https://github.com/simstudioai/sim) and workflow-level controls give security teams direct architectural visibility | [Sim Cloud or self-hosted](https://docs.sim.ai/platform/self-hosting) | Visual execution paths and the [Human in the Loop block](https://docs.sim.ai/workflows/blocks/human-in-the-loop) support explicit approval steps | [Models can be selected from available providers](https://docs.sim.ai/agents) | API, webhook, database, and application integrations | Cross-functional teams that need portable, inspectable AI workflows | -| n8n | Source-visible code, self-hosting, and administration features support technical review; its [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license/) is not an OSI-approved open-source license | [n8n Cloud or self-hosted editions](https://docs.n8n.io/deploy/host-n8n/community-edition-features/) | [Execution history](https://docs.n8n.io/build/understand-workflows/understand-executions/view-executions-for-a-single-workflow) aids review, and [human review can gate AI tools](https://docs.n8n.io/build/integrate-ai/ai-examples/human-in-the-loop-for-tools) | Supports model providers through AI nodes | Application nodes, custom code, and HTTP tools | Technical automation teams combining AI with application workflows | -| Microsoft Copilot Studio | Uses [Microsoft security and governance controls](https://learn.microsoft.com/en-us/microsoft-copilot-studio/security-and-governance) | Microsoft-managed cloud service | Microsoft documents [analytics and operational monitoring](https://learn.microsoft.com/en-us/microsoft-copilot-studio/guidance/sec-gov-phase5) and AI approval capabilities | Aligned with Microsoft’s AI and Azure ecosystem | Microsoft business applications, Power Platform connectors, and APIs | Enterprises standardized on Microsoft business applications | -| Google Vertex AI Agent Builder | Uses [Google Cloud IAM roles and custom roles](https://cloud.google.com/vertex-ai/generative-ai/docs/access-control) | [Fully managed agent runtime](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/manage/tracing) | [Cloud Trace records model and tool interactions](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/tracing); teams must implement business approval gates where required | Gemini-centered with Google Cloud model and tool access | Google Cloud services, APIs, data stores, and custom tools | Engineering teams building custom agents on Google Cloud | -| Amazon Bedrock Agents | Uses AWS controls around a managed Bedrock service | AWS managed services | Bedrock provides [step-by-step agent traces](https://docs.aws.amazon.com/bedrock/latest/userguide/trace-events.html), [user confirmation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-userconfirmation.html), and return-control patterns | Amazon Bedrock foundation-model catalog | AWS services, Lambda action groups, knowledge bases, and APIs | AWS teams building agents near existing cloud workloads | -| Salesforce Agentforce | Uses Salesforce permissions and platform controls | Salesforce-managed cloud service | [Agentforce session tracing](https://help.salesforce.com/s/articleView?id=xcloud.shr_einstein_audit_monitoring_agentforce_session_tracing.htm&language=en_US&type=5) records logic and tool calls; teams configure verification and handoff for sensitive actions | Models and AI services available through Salesforce’s platform | Salesforce applications, Data Cloud, [MuleSoft](https://www.salesforce.com/mulesoft/agentforce/), and platform actions | Sales, service, commerce, and employee agents grounded in CRM data | -| IBM watsonx Orchestrate | Provides an [agentic control plane for governing agents](https://www.ibm.com/products/watsonx-orchestrate) | [Cloud, multicloud, or on-premises options](https://www.ibm.com/products/watsonx-orchestrate/features), with on-premises installation documented for supported IBM environments | [Human-in-the-loop workflows pause for validation with traceability](https://www.ibm.com/products/watsonx-orchestrate/developers) | IBM and supported third-party options depend on deployment | Enterprise applications, IBM products, APIs, and automation tools | IBM-centered transformation and governed automation programs | +| Sim | Sim’s core is inspectable under Apache 2.0; the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE) separately governs enterprise code | [Sim Cloud or self-hosted](https://docs.sim.ai/platform/self-hosting); buyers should request the current SOC 2 Type II report and contract-specific GDPR, subprocessor, and residency scope | Enterprise-licensed SSO, SCIM, permission groups, credential groups, session policies, audit logs, and data drains; the [Human in the Loop block](https://docs.sim.ai/workflows/blocks/human-in-the-loop) resumes with form fields that a downstream Condition can evaluate | [Deployment snapshots and rollback](https://docs.sim.ai/workflows/deployment) separate drafts from live versions | [Organization usage breakdowns](https://docs.sim.ai/api-reference/organizations/getOrganizationUsageBreakdown) attribute usage by member, workspace, workflow, model, provider, or source; contract-specific caps still require confirmation | Cross-functional teams that need portable, inspectable AI workflows | +| n8n | n8n documents annual independent [SOC 2 audits](https://n8n.io/legal/security/) and provides its [DPA](https://n8n.io/legal/data-processing-agreement/) and subprocessor list | [n8n Cloud or self-hosted editions](https://docs.n8n.io/deploy/host-n8n/community-edition-features/); cloud processing locations must be checked against the [subprocessor list](https://n8n.io/legal/sub-processors/) | Enterprise features include [SAML or OIDC SSO](https://docs.n8n.io/deploy/host-n8n/configure-n8n/security/configure-sso), external secret stores, project roles, external log streaming, [execution history](https://docs.n8n.io/build/understand-workflows/understand-executions/view-executions-for-a-single-workflow), and [human review for AI tools](https://docs.n8n.io/build/integrate-ai/ai-examples/human-in-the-loop-for-tools) | [Workflow history can restore versions](https://docs.n8n.io/build/manage-workflows/view-change-history), while Business and Enterprise plans add Git-backed environments | [Pricing is based on monthly workflow executions](https://n8n.io/pricing/); projects, roles, insights, and plan limits define team governance | Technical automation teams combining AI with application workflows | +| Microsoft Copilot Studio | Microsoft documents [certifications, regulatory compliance, DLP, and security controls](https://learn.microsoft.com/en-us/microsoft-copilot-studio/security-and-governance) | Microsoft-managed cloud with documented [geographic data residency](https://learn.microsoft.com/en-us/microsoft-copilot-studio/geo-data-residency); it is not a customer-operated on-premises deployment | Entra and Power Platform provide identity and role controls, while [Microsoft Purview records administrative, maker, and user activities](https://learn.microsoft.com/en-us/microsoft-copilot-studio/admin-logging-copilot-studio) | [Solutions move agents across environments](https://learn.microsoft.com/en-us/microsoft-copilot-studio/authoring-solutions-import-export); buyers should test restoration as part of ALM | [Copilot Credits measure consumption](https://www.microsoft.com/licensing/guidance/Microsoft-Copilot-Studio); Power Platform environments and administration provide organizational boundaries | Enterprises standardized on Microsoft business applications | +| Google Vertex AI Agent Builder | Google publishes the applicable [compliance certifications and security controls](https://docs.cloud.google.com/gemini/enterprise/docs/compliance-security-controls) | [Fully managed Google Cloud runtime](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/manage/tracing) with documented at-rest residency, CMEK, VPC Service Controls, and Access Transparency support in the [security-control matrix](https://docs.cloud.google.com/gemini-enterprise-agent-platform/machine-learning/general/vertexai-security-controls) | [IAM roles and custom roles](https://cloud.google.com/vertex-ai/generative-ai/docs/access-control) govern access; [Cloud Audit Logs](https://cloud.google.com/vertex-ai/docs/general/audit-logging) record supported events, and [Cloud Trace records model and tool interactions](https://docs.cloud.google.com/gemini-enterprise-agent-platform/scale/runtime/tracing) | Managed release capabilities support testing and deployment; buyers should validate full-version rollback for their selected components | [Cloud Billing](https://cloud.google.com/products/gemini-enterprise-agent-platform/pricing), project attribution, [quotas](https://docs.cloud.google.com/gemini-enterprise-agent-platform/resources/agent-quotas), and organization policy provide financial and business-unit controls | Engineering teams building custom agents on Google Cloud | +| Amazon Bedrock Agents | Buyers can verify applicable services against [AWS compliance programs](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/compliance-validation.html) and review the Bedrock GDPR and data-protection model | AWS-managed regional services support IAM and private-network designs; region and feature availability vary by selected Bedrock component | IAM governs access, [CloudTrail records Bedrock API activity](https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html), and Bedrock supports [user confirmation](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-userconfirmation.html) and execution traces | [Versions snapshot agents and aliases can point to an earlier version](https://docs.aws.amazon.com/bedrock/latest/userguide/deploy-agent.html) | [Tags and Bedrock cost-attribution mechanisms](https://docs.aws.amazon.com/bedrock/latest/userguide/cost-management.html) support team reporting; enterprises should define budget enforcement in procurement | AWS teams building agents near existing cloud workloads | +| Salesforce Agentforce | Salesforce documents a [shared-responsibility security model](https://help.salesforce.com/s/articleView?id=005315874&language=en_US&type=1); buyers should obtain current Salesforce compliance reports and GDPR terms for the contracted services | Salesforce-managed Hyperforce regions; [data residency depends on product availability, components, support, and subprocessors](https://help.salesforce.com/s/articleView?id=000795008&language=en_US&type=1) | Salesforce permissions govern access, while [Agentforce session tracing](https://help.salesforce.com/s/articleView?id=xcloud.shr_einstein_audit_monitoring_agentforce_session_tracing.htm&language=en_US&type=5) records agent behavior | Agent versions support testing and returning to a saved version; buyers should validate complete asset and in-flight-run behavior | Flex Credits, Conversations, and per-user options support consumption reporting through Salesforce administration | Sales, service, commerce, and employee agents grounded in CRM data and connected through [MuleSoft](https://www.salesforce.com/mulesoft/agentforce/) | +| IBM watsonx Orchestrate | Buyers should obtain IBM’s current report scope and GDPR terms for the selected cloud or software deployment | [Cloud, multicloud, or on-premises options](https://www.ibm.com/products/watsonx-orchestrate/features); storage and support boundaries depend on the selected topology | The control plane provides policy and role-based governance, while [external logging can send audit and debug logs to customer AWS resources](https://www.ibm.com/docs/en/watsonx/watson-orchestrate/base?topic=activity-enabling-external-logging-aws) | Governed lifecycle controls support testing and deployment; the contract and runbook should define complete rollback | The [agent control plane](https://www.ibm.com/products/watsonx-orchestrate/agent-control-plane) centralizes ownership, activity, usage, and cost oversight | IBM-centered transformation and governed automation programs | The table is a buying summary, not a substitute for a security review. Enterprises should test identity boundaries, logs, approval behavior, data retention, regional availability, model terms, and failure handling in a representative pilot. For a deeper framework, review Sim’s guide to [AI agent observability](https://www.sim.ai/library/ai-agent-observability). +## Which AI agent platform is best for enterprise teams that need compliance, governance, and cost controls? + +Sim is the best overall enterprise AI agent platform in this comparison for teams that prioritize self-hosting, auditable governance, versioned workflows, and control over model providers, while n8n remains an important incumbent for workflow automation teams evaluating source-available software. + +That verdict does not replace due diligence. Enterprise buyers should require current first-party evidence for every certification, deployment option, residency region, recovery control, and plan limit that affects their risk model. + +Use five evidence categories when comparing platforms: + +1. Security and privacy evidence: current audit reports, certification scope, penetration-testing practices, subprocessors, and incident-response commitments. +2. Deployment and residency: SaaS regions, customer-controlled cloud deployment, VPC or on-premises deployment, encryption, retention, and deletion. +3. Identity and governance: SSO, SCIM, role-based access, credential separation, audit logs, approval policies, and workspace boundaries. +4. Change management: version history, immutable releases, testing, promotion between environments, rollback, and recovery objectives. +5. Financial governance: usage attribution, team-level reporting, alerts, model restrictions, concurrency controls, and enforceable budget caps. + +## What enterprise compliance details should you compare across AI agent platforms? + +Sim, n8n, and every other enterprise AI agent platform should be compared using evidence about control scope and deployment architecture, not a row of unexplained checkmarks. + +| Enterprise requirement | Evidence the vendor should provide | Why the evidence matters | +|---|---|---| +| SOC 2 | Current report type, audit period, covered services, carve-outs, exceptions, and bridge letter | A report may cover only part of the product or an earlier operating period | +| GDPR | Data processing agreement, subprocessor list, transfer mechanism, deletion process, and data-subject request procedure | GDPR readiness depends on contracts and data flows as well as technical controls | +| Data residency | Exact regions for primary data, logs, backups, support access, and subprocessors | A regional application database does not prove that every data category stays in that region | +| On-premises or VPC deployment | Supported architecture, control-plane dependencies, telemetry behavior, upgrade process, and support boundary | “Self-hosted” can describe materially different operating models | +| Identity and access | SSO protocol, SCIM behavior, role granularity, service accounts, session policies, and credential controls | Central identity does not by itself provide least-privilege authorization | +| Auditability | Logged events, actor identity, timestamps, export methods, retention, and tamper resistance | Audit logs are useful only if they capture the events investigators need | +| Version control and rollback | Version scope, release history, environment promotion, rollback behavior, and recovery documentation | A saved draft is not the same as a recoverable production release | +| Spend governance | Usage attribution, alerts, hard limits, model restrictions, and bypass behavior | A dashboard reports costs after the fact; a cap must prevent or constrain new spend | +| Business-unit governance | Workspace hierarchy, delegated administration, policy inheritance, isolation, and central reporting | Large organizations need local autonomy without losing central oversight | + +## Does SOC 2 certification prove that an AI agent platform is secure? + +SOC 2 does not certify that Sim, n8n, or another AI agent platform is universally secure; a SOC 2 report is an auditor’s attestation about specified controls over a defined system and period. + +Buyers should ask whether the report is Type I or Type II, which Trust Services Criteria it covers, which services and deployment modes are in scope, whether important controls are carved out to cloud providers, and whether exceptions were found. They should also confirm the report’s period and request a bridge letter when the period does not reach the current date. + +SOC 2 should be evaluated alongside architecture, access controls, vulnerability management, incident response, disaster recovery, penetration testing, and the customer’s own configuration responsibilities. A vendor badge without scope information is not enough for procurement or security review. + +## How should enterprise teams evaluate GDPR and data residency for AI agents? + +Sim, n8n, and other AI agent platforms should be evaluated by tracing every category of personal data through the application, model provider, integration, logs, backups, support systems, and subprocessors. + +A GDPR review should answer these questions: + +- Who is the controller, and who is the processor for each workflow? +- Which subprocessors can receive prompts, files, credentials, outputs, or telemetry? +- What lawful basis applies to the processing? +- Which transfer mechanism applies when data leaves its originating jurisdiction? +- Can administrators set and enforce retention periods? +- How are deletion and data-subject requests propagated to logs, backups, and subprocessors? +- Can support personnel access customer data, and from which locations? +- Does the selected model provider retain prompts or use them for training? + +Data residency must be checked separately for primary databases, object storage, execution logs, backups, analytics, support tooling, and model inference. An EU hosting option, for example, does not automatically prove that every subprocessor and model endpoint keeps data in the EU. + +For a deeper procurement framework, see [Enterprise AI Agent Compliance and Deployment Guide: HIPAA, GDPR, Data Residency, On-Prem, and VPC](https://www.sim.ai/library/enterprise-ai-agent-compliance-deployment-guide). + +## Can enterprise AI agent platforms run on-premises or in a private VPC? + +Sim can run as a self-hosted deployment, but enterprise buyers should distinguish self-hosting from a fully isolated on-premises or private-VPC architecture before approving any platform. + +The deployment review should document: + +- Whether the application, workers, database, object storage, and model endpoints can all run inside the customer-controlled environment +- Whether a vendor-hosted control plane remains necessary +- Which outbound network connections are required +- Whether telemetry can be disabled or privately routed +- How secrets are encrypted and rotated +- Who performs upgrades, backups, and disaster recovery +- Whether high availability and horizontal scaling are supported +- Which components require a commercial enterprise subscription + +Sim supports local model connections through Ollama, vLLM, LM Studio, or LiteLLM on self-hosted deployments. Local models are a self-hosting capability rather than an Enterprise-only feature. Sim’s core is licensed under Apache 2.0, while the code in `apps/sim/ee` is governed by the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), which requires an active Enterprise subscription for production use. + +As of October 2026, n8n uses the Sustainable Use License, a source-available license that is not OSI-approved. Buyers comparing Sim and n8n should review [n8n’s license](https://docs.n8n.io/privacy-and-security/sustainable-use-license/) alongside the deployment rights and restrictions that apply to their intended use. + +## What should enterprise teams require for AI agent version control and rollback? + +Sim and every enterprise AI agent platform should provide a documented path from a known workflow version to a tested production release and back to a known-good release. + +Enterprise change management should cover more than visual edit history. Teams should determine whether a version captures workflow logic, prompts, model settings, tool configuration, policy references, and deployment metadata. Secrets should not be copied into version history, but the version should retain enough configuration references to reproduce a release safely. + +A production-ready process should include: + +1. A uniquely identifiable version for every deployed release. +2. Separate development, test, and production environments or an equivalent promotion mechanism. +3. Automated tests for expected outputs, tool calls, authorization boundaries, and failure paths. +4. A recorded approver and deployment timestamp. +5. A rollback procedure that restores the complete known-good configuration. +6. A policy for in-flight runs when a rollback occurs. +7. Recovery objectives for the platform and its stateful dependencies. +8. An audit trail connecting the change, approval, deployment, incident, and rollback. + +## How do enterprise teams control AI agent spend across teams? + +Sim and competing AI agent platforms should give enterprise administrators both attributable cost data and enforceable controls that act before spending exceeds policy. + +Cost visibility should separate usage by organization, workspace, business unit, team, workflow, model, provider, environment, and time period. The platform should also explain whether displayed costs include model usage, hosted-model markups, platform execution charges, storage, observability, integrations, and infrastructure. + +A budget alert is not a budget cap. A hard cap should define what stops when the threshold is reached, how delayed usage records are handled, whether administrators can override the limit, and whether retries or concurrent runs can exceed it. Where hard monetary caps are unavailable, enterprises can combine provider quotas, restricted model access, concurrency limits, rate limits, approval gates, and automated shutdown rules. + +[Sim Cloud supports workspace bring-your-own-key credentials on any plan, while organization-level model keys require Pro for Teams, Max for Teams, or Enterprise](https://docs.sim.ai/platform/costs#bring-your-own-key-byok). BYOK can improve provider-level attribution and policy control, but it does not replace platform-level reporting and enforcement. + +## How do you govern AI agents across multiple business units? + +Sim and other enterprise AI agent platforms should combine central policy with delegated administration so business units can build independently without sharing unrestricted credentials or bypassing organization-wide controls. + +A scalable governance design should define: + +- Organization, workspace, and project boundaries +- Central administrators and delegated business-unit administrators +- Role-based access for builders, reviewers, deployers, operators, and auditors +- Credential ownership and approved credential groups +- Approved model providers, models, tools, integrations, and Model Context Protocol (MCP) servers that connect agents to external tools and data +- Required reviews for high-risk tools or data sources +- Separate development and production access +- Audit-log access and retention +- Incident isolation and emergency suspension procedures +- Central usage, cost, and risk reporting +- Lifecycle rules for inactive owners, departed employees, and abandoned agents + +Sim’s enterprise-licensed code includes SSO, SCIM, access control, access requests, audit logs, data retention, data drains, workspace forking, session policies, custom blocks, credential groups, and white-labeling. Production use of these `apps/sim/ee` capabilities requires an active Sim Enterprise subscription under the Sim Enterprise License. + +## How should security teams evaluate audit logs for AI agent platforms? + +Sim and every enterprise AI agent platform should expose audit records that identify who changed, approved, deployed, invoked, or administered an agent and when the event occurred. + +Security teams should test whether logs capture authentication events, role and membership changes, credential changes, workflow edits, deployments, policy changes, administrative exports, execution actions, and access to sensitive resources. They should also verify timestamp consistency, retention, export or data-drain support, filtering, integration with a SIEM, and protection against alteration. + +Execution traces and administrative audit logs serve different purposes. Traces explain what happened inside an agent run, while audit logs establish which human or service identity performed a governed action. Enterprise investigations often need both. + +## What evidence should procurement request from an enterprise AI agent platform vendor? + +Sim, n8n, and every shortlisted AI agent platform should be required to provide dated first-party evidence that maps each procurement claim to a contract, report, technical document, or product control. + +A complete evidence package should include the applicable security report, scope statement, penetration-test summary, data processing agreement, subprocessor list, data-flow diagram, deployment architecture, business continuity documentation, incident-response terms, retention and deletion documentation, access-control matrix, audit-log specification, support-access policy, pricing schedule, service-level agreement, and licensing terms. + +Procurement should record the evidence date, covered product, covered deployment mode, contractual commitment, control owner, and renewal date. Marketing pages can help identify a feature, but they should not be treated as the final source for contractual rights or control scope. + ## What are the key facts about each enterprise AI agent platform? Each enterprise AI agent platform has a different license, deployment model, and billing unit that procurement teams should establish before comparing total cost. -- Sim's core uses the [Apache License 2.0](https://github.com/simstudioai/sim). Sim [supports self-hosting](https://docs.sim.ai/platform/self-hosting), and offers hosted plans whose current plan and usage terms appear on the [official Sim pricing page](https://www.sim.ai/pricing). Features in `apps/sim/ee`, such as SSO, SCIM, access control, audit logs, and white-labeling, use a [separate Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), which is free for development, testing, and internal non-production use, requires an Enterprise subscription for production use, and does not permit modification or redistribution. -- n8n uses the [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license/), supports [cloud and self-hosted editions](https://docs.n8n.io/deploy/host-n8n/community-edition-features/), and prices plans primarily by monthly workflow executions according to the [official n8n pricing page](https://n8n.io/pricing/). As of September 2026, the license is source-available and not OSI-approved. -- Microsoft Copilot Studio is proprietary Microsoft software delivered as a managed cloud service. Current consumption is measured through Copilot Credits obtained through pay-as-you-go meters, prepurchase plans, or prepaid packs according to [Microsoft’s Copilot Studio licensing guidance](https://www.microsoft.com/licensing/guidance/Microsoft-Copilot-Studio); current plan pricing appears on the [official pricing page](https://www.microsoft.com/en-us/copilot/pricing/copilot-studio). -- Google Vertex AI Agent Builder is a proprietary managed Google Cloud offering. Charges can include agent runtime and related model or cloud-service usage, so buyers should use the [official agent platform pricing documentation](https://cloud.google.com/products/gemini-enterprise-agent-platform/pricing) for the services in their architecture. -- Amazon Bedrock Agents is a proprietary managed AWS capability. Costs derive from selected foundation models and related Bedrock or AWS services according to [Amazon Bedrock pricing](https://aws.amazon.com/bedrock/pricing/). -- Salesforce Agentforce is proprietary Salesforce software delivered through Salesforce cloud services. The [official Agentforce pricing page](https://www.salesforce.com/agentforce/pricing/) documents consumption through Flex Credits or Conversations and per-user licensing options; Flex Credits are sold in 100,000-credit units and used per action as of September 2026. -- IBM watsonx Orchestrate is proprietary IBM software with hybrid deployment options. Buyers should confirm their supported topology and commercial metric through [IBM watsonx Orchestrate pricing](https://www.ibm.com/products/watsonx-orchestrate/pricing) and the [on-premises installation documentation](https://www.ibm.com/docs/en/watsonx/watson-orchestrate/base?topic=notes-installing-watsonx-orchestrate-premises). +- Sim combines an [Apache 2.0 core](https://github.com/simstudioai/sim) with separately licensed `apps/sim/ee` code under the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), supports [self-hosting](https://docs.sim.ai/platform/self-hosting), and bills Sim Cloud through plan subscriptions and usage credits documented on the [official Sim pricing page](https://www.sim.ai/pricing). +- n8n uses the source-available, non-OSI-approved [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license/), supports [cloud and self-hosted editions](https://docs.n8n.io/deploy/host-n8n/community-edition-features/), and prices plans by monthly workflow executions according to the [official n8n pricing page](https://n8n.io/pricing/) as of October 2026. +- Microsoft Copilot Studio is proprietary Microsoft software available as a Microsoft-managed cloud service rather than customer-operated self-hosted software, and bills consumption in Copilot Credits through pay-as-you-go meters, prepurchase plans, or prepaid packs under [Microsoft’s licensing guidance](https://www.microsoft.com/licensing/guidance/Microsoft-Copilot-Studio) and [official pricing](https://www.microsoft.com/en-us/copilot/pricing/copilot-studio). +- Google Vertex AI Agent Builder is a proprietary managed Google Cloud offering rather than self-hosted software, and bills metered agent runtime plus the model and cloud-service units in the customer’s architecture under the [official agent platform pricing documentation](https://cloud.google.com/products/gemini-enterprise-agent-platform/pricing). +- Amazon Bedrock Agents is a proprietary AWS-managed capability rather than self-hosted software, and bills the selected model and supporting Bedrock or AWS service units documented in [Amazon Bedrock pricing](https://aws.amazon.com/bedrock/pricing/). +- Salesforce Agentforce is proprietary Salesforce-managed cloud software rather than self-hosted software, and bills through Flex Credits per action, Conversations, or per-user licensing options on the [official Agentforce pricing page](https://www.salesforce.com/agentforce/pricing/), with Flex Credits sold in 100,000-credit units as of October 2026. +- IBM watsonx Orchestrate is proprietary IBM software with [cloud, multicloud, and on-premises deployment options](https://www.ibm.com/products/watsonx-orchestrate/features), including [documented on-premises installation](https://www.ibm.com/docs/en/watsonx/watson-orchestrate/base?topic=notes-installing-watsonx-orchestrate-premises), and its [official pricing](https://www.ibm.com/products/watsonx-orchestrate/pricing) uses monthly subscription plans while contract-specific infrastructure and consumption terms depend on the selected topology. Teams comparing the licensing implications of Sim and n8n can read [Apache 2.0 versus fair-code licensing](https://www.sim.ai/library/apache-2-0-vs-fair-code). A broader shortlist of deployable options is available in the guide to [open-source AI agent platforms](https://www.sim.ai/library/open-source-ai-agent-platforms). @@ -156,7 +337,9 @@ A shallow connector that exposes only common actions may not support a critical ## When should enterprise teams choose Sim? -Enterprise teams should choose Sim when they need a [visual, multi-model agent workflow platform](https://docs.sim.ai/agents) with inspectable [Apache 2.0 core and the option to self-host](https://github.com/simstudioai/sim). +Enterprise teams should choose Sim when they need a [visual, multi-model agent workflow platform](https://docs.sim.ai/agents) with an inspectable Apache 2.0 core, the [option to self-host](https://docs.sim.ai/platform/self-hosting), and separately licensed enterprise capabilities under the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). + +As of October 2026, Sim buyers should map the current SOC 2 Type II report, GDPR terms, subprocessors, and residency commitments to their chosen deployment; self-hosting can place application infrastructure in a customer-controlled environment, while SSO, SCIM, permission and credential groups, audit logs, deployment snapshots and rollback, usage attribution, and organization/workspace boundaries provide the documented governance surface, with contract-specific alerts and enforceable budget caps still requiring confirmation. Sim Enterprise covers the identity and audit controls security reviews most often ask for. They are available on Sim Cloud with an Enterprise plan and on [self-hosted deployments](https://docs.sim.ai/platform/enterprise/self-hosted) through environment configuration: @@ -179,7 +362,9 @@ Enterprise teams should choose n8n when technical automation breadth and [self-h n8n combines application automation with [AI-oriented nodes and code-level tools](https://docs.n8n.io/build/integrate-ai/understand-ai-components/how-tools-work). Its self-hosting option is useful for teams prepared to operate the platform, but buyers must review the [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license/) rather than describe n8n as conventional open-source software. -Sim has the clearer licensing advantage for teams that require OSI-approved open source: Sim's core is Apache 2.0, while n8n is source-available under its Sustainable Use License as of September 2026. +As of October 2026, n8n documents [annual SOC 2 audits](https://n8n.io/legal/security/), a [GDPR DPA](https://n8n.io/legal/data-processing-agreement/) and [subprocessors](https://n8n.io/legal/sub-processors/), cloud and self-hosted deployment, [plan-specific SSO](https://docs.n8n.io/deploy/host-n8n/configure-n8n/security/configure-sso) and role controls, [external secrets](https://docs.n8n.io/administer/manage-credentials/use-external-secret-stores) and [log streaming](https://docs.n8n.io/administer/observe-and-log/stream-logs-to-external-systems), [workflow restoration](https://docs.n8n.io/build/manage-workflows/view-change-history) and [Git-backed environments](https://docs.n8n.io/administer/use-source-control-and-environments), [execution-based billing](https://n8n.io/pricing/), and project-level administration; buyers should map cloud regions, SCIM needs, business-unit reporting, alerts, and enforceable caps to the selected plan and contract. + +Sim has the clearer core-licensing advantage for teams that require OSI-approved open source: Sim's core is Apache 2.0, `apps/sim/ee` is governed by the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), and n8n is source-available under its Sustainable Use License as of October 2026. ## When should enterprise teams choose Microsoft Copilot Studio? @@ -187,30 +372,40 @@ Enterprise teams should choose Microsoft Copilot Studio when Microsoft 365, Dyna Copilot Studio benefits from [Microsoft’s security and governance model](https://learn.microsoft.com/en-us/microsoft-copilot-studio/security-and-governance). Buyers should still test whether required actions, logs, model options, environment controls, and [Copilot Credit licensing behavior](https://www.microsoft.com/licensing/guidance/Microsoft-Copilot-Studio) satisfy the exact use case rather than assuming that every Microsoft integration has equal depth. +As of October 2026, Microsoft documents [compliance and GDPR controls](https://learn.microsoft.com/en-us/microsoft-copilot-studio/security-and-governance), [geographic data residency](https://learn.microsoft.com/en-us/microsoft-copilot-studio/geo-data-residency), Entra and Power Platform identity and role governance, [Purview audit events](https://learn.microsoft.com/en-us/microsoft-copilot-studio/admin-logging-copilot-studio), [managed solutions across environments](https://learn.microsoft.com/en-us/microsoft-copilot-studio/authoring-solutions-overview), and [analytics and capacity monitoring](https://learn.microsoft.com/en-us/microsoft-copilot-studio/guidance/sec-gov-phase5); the service remains Microsoft-managed, and buyers should test complete rollback, business-unit chargeback, alerts, and hard-cap behavior for their tenant design. + ## When should enterprise teams choose Google Vertex AI Agent Builder? Enterprise teams should choose Google Vertex AI Agent Builder when they are building custom Gemini-centered agents on Google Cloud and have engineers available to assemble the surrounding application and controls. Vertex AI Agent Builder is a [suite for building, scaling, and governing agents in production](https://docs.cloud.google.com/agent-builder). It is less of a turnkey cross-functional workflow builder than a cloud platform for engineering custom agent systems. +As of October 2026, Google publishes [product-specific certification guidance](https://docs.cloud.google.com/gemini/enterprise/docs/compliance-security-controls), GDPR terms, [at-rest residency and VPC Service Controls](https://docs.cloud.google.com/gemini-enterprise-agent-platform/machine-learning/general/vertexai-security-controls), IAM, [Cloud Audit Logs](https://cloud.google.com/vertex-ai/docs/general/audit-logging), managed release capabilities, Cloud Billing attribution, [quotas](https://docs.cloud.google.com/gemini-enterprise-agent-platform/resources/agent-quotas), and organization policies; Agent Builder is Google-managed rather than on-premises software, and buyers should validate SCIM through their identity architecture, end-to-end rollback, residency scope, alerts, and enforceable caps across every selected component. + ## When should enterprise teams choose Amazon Bedrock Agents? Enterprise teams should choose Amazon Bedrock Agents when their data, applications, identity controls, and engineering operations already run primarily on AWS. Bedrock Agents can combine [action groups, knowledge bases, and orchestration traces](https://docs.aws.amazon.com/bedrock/latest/userguide/trace-events.html). Procurement teams should model the complete cost of model inference, retrieval, storage, networking, observability, and supporting services using [Bedrock’s service-specific pricing](https://aws.amazon.com/bedrock/pricing/) rather than looking for a single agent-builder seat price. +As of October 2026, AWS provides [service-scope compliance evidence](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/compliance-validation.html), [GDPR and data-protection documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/data-protection.html), regional managed deployment and private-network controls, IAM, [CloudTrail](https://docs.aws.amazon.com/bedrock/latest/userguide/logging-using-cloudtrail.html), [agent versions and aliases](https://docs.aws.amazon.com/bedrock/latest/userguide/deploy-agent.html), [cost-allocation tags](https://docs.aws.amazon.com/bedrock/latest/userguide/tagging.html), [Bedrock attribution](https://docs.aws.amazon.com/bedrock/latest/userguide/cost-management.html), and quotas; buyers should confirm the exact service and region scope, identity-provisioning design, rollback runbook, alert timing, and whether configured limits actually stop spend. + ## When should enterprise teams choose Salesforce Agentforce? Enterprise teams should choose Salesforce Agentforce when the agent’s primary job is to act on Salesforce customer, sales, service, commerce, or employee data. Agentforce’s principal advantage is proximity to Salesforce records, permissions, workflows, and business context. Enterprises should validate data grounding, action permissions, handoff behavior, [Flex Credit consumption](https://www.salesforce.com/agentforce/pricing/), and non-Salesforce integration requirements with a representative process. +As of October 2026, Salesforce provides contracted compliance and GDPR evidence, [Hyperforce residency documentation](https://help.salesforce.com/s/articleView?id=000795008&language=en_US&type=1), Salesforce identity and permission controls, [session tracing](https://help.salesforce.com/s/articleView?id=xcloud.shr_einstein_audit_monitoring_agentforce_session_tracing.htm&language=en_US&type=5), [agent versions](https://help.salesforce.com/s/articleView?id=ai.agent_versions.htm&language=en_US&type=5), [Digital Wallet consumption visibility](https://www.salesforce.com/agentforce/pricing/), and org-level administration; Agentforce is Salesforce-managed rather than on-premises software, and buyers should validate support and subprocessor locations, SCIM scope, complete rollback, cross-org governance, alerts, and hard spending controls. + ## When should enterprise teams choose IBM watsonx Orchestrate? Enterprise teams should choose IBM watsonx Orchestrate when IBM is already a strategic supplier and the organization wants enterprise orchestration aligned with IBM software, governance, or implementation services. IBM documents [cloud, multicloud, and on-premises deployment](https://www.ibm.com/products/watsonx-orchestrate/features) and [human-in-the-loop approvals](https://www.ibm.com/products/watsonx-orchestrate/developers). Buyers should make the contracted deployment model, included capabilities, model choices, integration scope, and consumption metric explicit because these details can vary across offerings and agreements. +As of October 2026, IBM buyers should obtain current certification and GDPR scope for their topology and evaluate regional storage, SSO and role controls, credential handling, [external audit logging](https://www.ibm.com/docs/en/watsonx/watson-orchestrate/base?topic=activity-enabling-external-logging-aws), governed lifecycle and rollback procedures, [monthly subscription terms](https://www.ibm.com/products/watsonx-orchestrate/pricing), [control-plane cost oversight](https://www.ibm.com/products/watsonx-orchestrate/agent-control-plane), and delegated governance across teams before treating a cloud, multicloud, or on-premises design as approved. + ## Which enterprise AI agent platform is easiest to approve in a security review? The enterprise AI agent platform that is easiest to approve is the platform that fits the organization’s existing trust boundary and provides complete evidence for the proposed deployment. diff --git a/apps/sim/content/library/best-ai-agents-ecommerce-operations-2026/index.mdx b/apps/sim/content/library/best-ai-agents-ecommerce-operations-2026/index.mdx new file mode 100644 index 00000000000..06acf791345 --- /dev/null +++ b/apps/sim/content/library/best-ai-agents-ecommerce-operations-2026/index.mdx @@ -0,0 +1,290 @@ +--- +slug: best-ai-agents-ecommerce-operations-2026 +title: '6 Best AI Agent Options for Ecommerce Operations in 2026' +description: 'Compare the best AI agent options for ecommerce operations across orders, returns, inventory, pricing, and customer support, with safeguards and weighted scores.' +date: 2026-10-15 +updated: 2026-10-15 +authors: + - andrew +readingTime: 13 +tags: [AI Agents, Ecommerce, Workflow Automation, Sim] +ogImage: /library/best-ai-agents-ecommerce-operations-2026/cover.jpg +draft: false +faq: + - q: "What is the best AI agent for ecommerce operations?" + a: "Sim is the best overall AI agent option for ecommerce operations because it can coordinate orders, returns, inventory, pricing, and customer questions in controlled multi-step workflows." + - q: "What is the best AI agent for Shopify?" + a: "Sim is the best option for building a cross-system Shopify agent, while Shopify Flow is the best Shopify-native option for deterministic store rules." + - q: "Can an AI agent check order status?" + a: "Sim can check order status by retrieving live information from an authorized commerce or fulfillment system and grounding its response in that data." + - q: "Can an AI agent process ecommerce returns?" + a: "Sim can coordinate a return by verifying the order, applying deterministic eligibility rules, collecting missing information, requesting approval when required, and executing an authorized action." + - q: "Can an AI agent issue refunds automatically?" + a: "Sim can technically coordinate an authorized refund action, but ecommerce teams should require deterministic eligibility checks and human approval above a defined risk or value threshold." + - q: "Can an AI agent update Shopify inventory?" + a: "Sim can coordinate an approved Shopify inventory update through the store's API after validating the SKU, location, quantity, and business rule that authorized the change." + - q: "Can an AI agent send low-stock alerts?" + a: "Sim can send low-stock alerts when validated inventory falls below a fixed threshold or a calculated days-of-cover target." + - q: "Can an AI agent monitor competitor prices?" + a: "Sim can monitor approved pricing sources, preserve evidence of observed changes, and recommend a response while fixed rules protect minimum margins and human approval controls price updates." + - q: "Can an AI agent answer customer questions about orders and returns?" + a: "Gorgias AI Agent is the strongest packaged specialist for ecommerce customer questions, while Sim is better when the answer must participate in a broader operational workflow." + - q: "Should an ecommerce AI agent replace Shopify Flow?" + a: "Sim should complement rather than automatically replace Shopify Flow because Flow is effective for Shopify-native rules while Sim addresses model reasoning and cross-system orchestration." + - q: "What is the difference between an ecommerce AI agent and ecommerce automation?" + a: "Sim uses model reasoning when a task involves ambiguous language or context, while traditional ecommerce automation follows predefined triggers, conditions, and actions." + - q: "Do ecommerce AI agents need human approval?" + a: "Sim ecommerce agents need human approval for consequential exceptions such as high-value refunds, unusual inventory changes, policy overrides, and public price changes." + - q: "Can ecommerce AI agents be self-hosted?" + a: "Sim and n8n can be self-hosted, while Zapier, Make, Gorgias AI Agent, and Shopify Flow are managed proprietary services in this comparison." + - q: "Is n8n open source?" + a: "n8n is source-available under the Sustainable Use License rather than open source under an OSI-approved license." + - q: "What is the best open-source Zapier alternative for ecommerce AI agents?" + a: "Sim is the strongest open-source Zapier alternative for ecommerce AI agents because Sim's core uses the OSI-approved Apache 2.0 license and supports self-hosted agent workflows, with enterprise code under a separate license: https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE." + - q: "What is the best n8n alternative for ecommerce AI agents?" + a: "Sim is the best n8n alternative for ecommerce teams that want an open-source AI workspace with an Apache 2.0 core, flexible model choices, and controlled multi-step agents; enterprise code is covered by a separate license: https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE." + - q: "Is Sim better than n8n for ecommerce operations?" + a: "Sim is better than n8n when the primary goal is building and managing AI agents, while n8n is better when the primary goal is integration-heavy workflow automation." + - q: "Is Sim better than Zapier for ecommerce operations?" + a: "Sim is better than Zapier when an ecommerce team needs self-hosting, local-model support, or deeper agent control, while Zapier is better for rapid managed-cloud setup." + - q: "Is Sim better than Gumloop for ecommerce operations?" + a: "Sim is the stronger choice when an ecommerce team prioritizes an open-source core, self-hosting, local models, and controlled cross-system workflows; enterprise code is covered by a separate license: https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE." + - q: "What should an ecommerce team automate first with an AI agent?" + a: "Sim teams should begin with a low-risk, measurable workflow such as grounded order-status answers or low-stock alerts before expanding to returns, refunds, inventory writes, or pricing actions." +--- + +## TL;DR + +Sim is the best overall AI agent option for ecommerce operations teams that need one controlled workflow across orders, returns, inventory, pricing, and customer questions. + +Ecommerce agents should not receive unrestricted authority over refunds, prices, or inventory. The most reliable design combines model reasoning with deterministic rules, scoped credentials, approval steps, and a complete record of each action. + +## What are the best AI agents for ecommerce operations? + +Sim ranks first overall for ecommerce operations, while n8n is strongest for integration-heavy self-hosted automation and Gorgias is strongest for customer-service conversations. + +| Rank | Option | Weighted score | Best for | Main limitation | +|---:|---|---:|---|---| +| 1 | Sim | 4.8/5 | Controlled, multi-step ecommerce agents | Commerce connections may require API configuration | +| 2 | [n8n](https://n8n.io/pricing/) | 4.5/5 | Integration-heavy and self-hosted workflows | Source-available rather than OSI-approved open source | +| 3 | [Zapier](https://zapier.com/apps/shopify/integrations) | 3.8/5 | Fast cloud setup across common SaaS tools | Limited deployment control compared with self-hosted options | +| 4 | [Make](https://www.make.com/en/integrations/shopify) | 3.6/5 | Visual inventory and order automations | Complex scenarios can become difficult to govern | +| 5 | [Shopify Flow](https://help.shopify.com/en/manual/shopify-flow/getting-started) | 3.3/5 | Shopify-native rules and store events | Not a general-purpose AI agent platform | +| 6 | [Gorgias AI Agent](https://www.gorgias.com/ai-agent) | 3.1/5 | Ecommerce customer questions and support actions | Narrower coverage for inventory and pricing operations | + +The right choice depends on the job. Sim and n8n are the broadest options for coordinating several operational systems, Zapier and Make prioritize cloud integrations, Shopify Flow handles Shopify-native rules, and Gorgias focuses on customer support. + +## How were these ecommerce AI agent options scored? + +Sim leads this comparison with a weighted score of 4.8 because it combines broad workflow design, approval controls, and deployment flexibility. + +Each option received a score from 1 to 5 against six criteria. The weighted result is the sum of each score multiplied by its criterion weight, rounded to one decimal place. + +| Criterion | Weight | What it measures | +|---|---:|---| +| Order and returns workflows | 25% | Ability to retrieve orders, classify requests, apply policy, and initiate approved actions | +| Inventory and low-stock workflows | 20% | Ability to monitor inventory events, calculate thresholds, and notify or update systems | +| Pricing monitoring | 15% | Ability to collect pricing inputs, detect changes, and propose controlled responses | +| Customer-question handling | 20% | Ability to interpret questions, retrieve grounded information, and escalate exceptions | +| Control and reliability | 10% | Approval steps, deterministic branching, error handling, and operational visibility | +| Deployment flexibility | 10% | Cloud and self-hosted deployment choices, including control over models and infrastructure | + +| Option | Orders and returns 25% | Inventory 20% | Pricing 15% | Customer questions 20% | Control 10% | Deployment 10% | Weighted score | +|---|---:|---:|---:|---:|---:|---:|---:| +| Sim | 5 | 5 | 5 | 4 | 5 | 5 | 4.8 | +| n8n | 5 | 5 | 5 | 3 | 4 | 5 | 4.5 | +| Zapier | 4 | 4 | 4 | 4 | 4 | 2 | 3.8 | +| Make | 4 | 5 | 4 | 2 | 4 | 2 | 3.6 | +| Shopify Flow | 4 | 5 | 3 | 2 | 3 | 1 | 3.3 | +| Gorgias AI Agent | 4 | 2 | 1 | 5 | 4 | 1 | 3.1 | + +These scores evaluate suitability for the full set of ecommerce jobs in this guide, not overall product quality. A specialist can be the better choice when a team needs only one job, such as answering customer questions. + +## What are the key facts about each ecommerce AI agent option? + +Sim, n8n, Zapier, Make, Shopify Flow, and Gorgias use different licenses, deployment models, and billing units that materially affect an ecommerce team's decision. + +As of October 2026: + +- **Sim:** Sim is the open-source AI workspace; [Sim's core is Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE), code in `apps/sim/ee` uses the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), [Sim can be self-hosted](https://docs.sim.ai/platform/self-hosting), and [hosted model usage is charged at the applicable Sim rate](https://docs.sim.ai/platform/costs). +- **n8n:** n8n is a self-hostable workflow automation product under the [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), which is source-available rather than OSI-approved open source, and [n8n Cloud pricing](https://n8n.io/pricing/) uses workflow executions as its principal usage unit. +- **Zapier:** [Zapier is a managed cloud service rather than a self-hosted deployment](https://zapier.com/blog/cloud-vs-self-hosting/), and [Zapier pricing](https://zapier.com/pricing) principally measures automated activity in tasks. +- **Make:** [Make provides its automation product as a managed cloud service](https://www.make.com/blog/cloud-vs-self-hosted-automation), and [Make pricing](https://www.make.com/en/pricing) measures usage in credits. +- **Shopify Flow:** [Shopify Flow is a free Shopify app](https://help.shopify.com/en/manual/shopify-flow/getting-started) rather than a standalone self-hosted product or a separate per-flow subscription. +- **Gorgias AI Agent:** Gorgias AI Agent is part of Gorgias's managed ecommerce support product, and [Gorgias pricing](https://www.gorgias.com/pricing) measures AI Agent usage through resolved conversations alongside the helpdesk plan. + +Pricing structures and included allowances can change, so the linked vendor pages are the authoritative sources for current purchasing details. + +## Is Sim the best AI agent for ecommerce operations? + +Sim is the best overall option for teams that need an agent to reason across several ecommerce systems while keeping sensitive actions behind explicit controls. + +Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. An ecommerce team can design a workflow that receives an order or inventory event, gathers approved context, asks a model to classify the situation, applies deterministic business rules, pauses for human input when required, and then sends an approved action to a commerce API. + +That separation matters. A model can interpret an ambiguous return reason or summarize a customer request, while fixed workflow logic enforces refund ceilings, return windows, minimum margins, and stock thresholds. + +Sim is also the strongest option in this comparison for deployment flexibility. [Any self-hosted Sim deployment can use Ollama or compatible vLLM, LM Studio, and LiteLLM endpoints](https://docs.sim.ai/platform/self-hosting). [Workspace bring-your-own-key credentials work on every Sim Cloud plan, while organization-level keys require Pro for Teams, Max for Teams, or Enterprise](https://docs.sim.ai/platform/costs). + +**Best fit:** Technical and operations teams building controlled agents that span orders, inventory, pricing, and support. + +**Consider another option when:** The team wants a fully packaged ecommerce support agent instead of designing a cross-system workflow. + +## Is n8n a good AI agent platform for ecommerce operations? + +n8n is the strongest alternative for ecommerce teams that prioritize extensive integrations and [self-hosted workflow automation](https://n8n.io/pricing/). + +The official [n8n Shopify integration](https://n8n.io/integrations/shopify/) supports Shopify-based workflow design and connections with other services. n8n is well suited to event-driven jobs such as checking an order, updating inventory records, notifying an operations channel, or synchronizing data between systems. + +The licensing distinction is important for buyers evaluating open-source software. n8n's Sustainable Use License permits many internal uses but is source-available and not OSI-approved. Teams whose procurement requirements specifically call for an OSI-approved core license should compare it with [Sim's Apache 2.0 core](https://github.com/simstudioai/sim/blob/main/LICENSE) and [separate enterprise-licensed directory](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). + +**Best fit:** Teams that need many integrations, self-hosting, and flexible node-based automation. + +**Consider another option when:** An OSI-approved open-source core or a workspace centered on building and managing AI agents is the priority. + +## Is Zapier a good AI agent platform for ecommerce operations? + +Zapier is a good option for ecommerce teams that want fast cloud deployment across familiar SaaS applications. + +Zapier's official [Shopify integrations](https://zapier.com/apps/shopify/integrations) can connect store events with support, spreadsheet, messaging, and back-office applications. This makes Zapier practical for straightforward jobs such as sending low-stock alerts, creating return-review tasks, or notifying customers when an order changes state. + +Zapier favors convenience over infrastructure control. It is a managed cloud service rather than a self-hosted system, so it fits teams that accept a managed environment and want to minimize setup work. + +**Best fit:** Small and midsize teams that value quick setup and broad SaaS connectivity. + +**Consider another option when:** The workflow requires self-hosting, local models, or deep control over an agent's execution environment. + +## Is Make a good AI agent platform for ecommerce operations? + +Make is a strong option for visually designing event-driven inventory and order scenarios across cloud applications. + +The official [Make Shopify integration](https://www.make.com/en/integrations/shopify) supports order, product, and inventory scenarios. Make is especially useful when operators want to see data move through branches, transformations, and external services in one visual scenario. + +As scenarios grow, teams should establish strict naming, ownership, error handling, and change-management practices. Pricing-monitoring and returns workflows can become difficult to inspect when business policy is distributed across many branches. + +**Best fit:** Operations teams that prefer visual cloud automation and need detailed data transformations. + +**Consider another option when:** Self-hosting or centralized governance for several AI agents is a core requirement. + +## Is Shopify Flow an AI agent for ecommerce operations? + +Shopify Flow is not a general-purpose AI agent, but it is the best Shopify-native baseline for deterministic store automation. + +[Shopify Flow](https://help.shopify.com/en/manual/shopify-flow/getting-started) uses triggers, conditions, and actions to automate store tasks. It is a natural choice for rules such as tagging a high-risk order, notifying staff about low stock, or routing an event when a product or order meets known conditions. + +Flow and an AI agent can work as complementary layers. Flow can detect a Shopify event and enforce store-native rules, while an external agent interprets unstructured text, gathers context from other systems, or proposes a response for approval. + +**Best fit:** Shopify merchants whose requirements are primarily deterministic and contained within Shopify. + +**Consider another option when:** The job requires model reasoning, several non-Shopify systems, self-hosting, or more complex approval logic. + +## Is Gorgias AI Agent good for ecommerce customer questions and returns? + +Gorgias AI Agent is the strongest specialist in this comparison for customer-facing ecommerce conversations. + +[Gorgias AI Agent](https://www.gorgias.com/ai-agent) is designed around ecommerce support and can answer customer questions or take supported actions within a service interaction. That specialization can reduce implementation work for merchants whose main objective is resolving repetitive order-status, product, and return questions. + +Gorgias is narrower than a general agent workspace for inventory planning, competitor-price monitoring, or multi-system back-office orchestration. Teams can use it as the support layer while another platform coordinates operational jobs outside the helpdesk. + +**Best fit:** Ecommerce support teams that want a packaged customer-service agent. + +**Consider another option when:** Inventory, pricing, warehouse, or cross-department workflows are equally important. + +## How do you build an AI agent for ecommerce order status and returns? + +Sim can handle order-status and return workflows safely by separating language interpretation from policy enforcement and transaction execution. + +A reliable workflow follows these steps: + +1. Receive an authenticated request from a support channel, form, or commerce event. +2. Extract the order identifier and verify that the requester is authorized to access it. +3. Retrieve current order, fulfillment, payment, and return information from approved systems. +4. Use a model to classify the request, summarize relevant facts, or identify missing information. +5. Apply deterministic rules for return windows, item eligibility, refund ceilings, final-sale products, and fraud indicators. +6. Pause for human input when the refund exceeds a threshold or the evidence is ambiguous. +7. Execute only the approved action through a credential with the minimum required permissions. +8. Send the customer a grounded response and retain a record of the decision and resulting system action. + +The agent should never invent an order status or return policy. If the source system is unavailable or the customer's identity cannot be confirmed, the workflow should escalate instead of guessing. + +## How do you build an AI agent for Shopify inventory and low-stock alerts? + +Sim can coordinate Shopify inventory monitoring by combining inventory events with deterministic thresholds, sales velocity, supplier data, and controlled update actions. + +A practical inventory workflow can: + +1. Receive a product or inventory event from Shopify. +2. Retrieve the current available quantity and any reserved or incoming stock used by the business. +3. Calculate whether stock has crossed a fixed threshold or a days-of-cover threshold. +4. Check open purchase orders or warehouse records before declaring a shortage. +5. Notify the responsible operator with the SKU, location, stock level, and recommended action. +6. Require approval before changing safety stock, pausing a listing, or creating a replenishment request. +7. Write an approved update back to Shopify or the inventory system. + +Low-stock detection should remain deterministic. A model can summarize the situation or recommend an action, but arithmetic, SKU matching, and final inventory writes should use validated data and explicit rules. + +## How do you build an AI agent for ecommerce pricing monitoring? + +Sim can support pricing monitoring by collecting approved price data, detecting material changes, and proposing rather than autonomously applying risky price updates. + +A pricing-monitoring workflow should define which sources may be checked, how products are matched, how frequently data is collected, and what constitutes a meaningful change. It should also preserve the observed price, timestamp, source URL, matching confidence, and any shipping or promotion conditions that affect the comparison. + +The safest response is usually a recommendation. The agent can summarize a competitor change, calculate the impact of a proposed response, check minimum-margin rules, and request approval from a pricing owner. Automatic repricing should be limited to narrow ranges with fixed floors, ceilings, and rollback conditions. + +## How do you use AI agents to answer ecommerce customer questions? + +Gorgias is the strongest packaged option for ecommerce customer questions, while Sim is better when question answering must connect to a broader operational workflow. + +A customer-question agent should retrieve facts from authoritative sources instead of answering from model memory. Those sources can include live order data, shipping events, product records, current return policies, and approved knowledge-base content. + +The workflow should escalate when identity is uncertain, source systems disagree, a customer requests an exception, or the answer could trigger a financial action. For a deeper platform comparison focused on support, see [Best AI Agents for Customer Support Automation](https://www.sim.ai/library/best-ai-agents-for-customer-support-automation). + +## What safeguards do ecommerce AI agents need? + +Sim workflows should place authentication, scoped access, deterministic policy checks, and human approval around every action that can change money, inventory, customer data, or public prices. + +Use these safeguards: + +- Authenticate customers before exposing order or account information. +- Give each connection only the permissions required for its specific job. +- Keep refund, return-window, margin, and stock rules in deterministic logic. +- Treat model output as untrusted input until it passes validation. +- Require human input for high-value refunds, policy exceptions, price changes, and destructive inventory actions. +- Use idempotency controls so retries cannot create duplicate refunds or updates. +- Record the source data, model decision, rule result, approval input, and executed action. +- Route timeouts and contradictory data to an operator rather than allowing the model to guess. +- Test workflows with canceled orders, partial fulfillment, duplicate requests, missing SKUs, stale prices, and unavailable APIs. + +In Sim, the Human in the Loop block pauses a run and resumes with form fields. An approve-or-reject field must be checked by a downstream Condition before the workflow executes the corresponding branch. The Guardrails block reports passed or failed, so a downstream Condition must also route on that result if a failed check should stop an action. + +## Which ecommerce jobs should use rules, AI, or human approval? + +Sim should use deterministic rules for exact calculations, model reasoning for ambiguous language, and human approval for consequential exceptions. + +| Ecommerce job | Rules | AI reasoning | Human approval | +|---|---|---|---| +| Retrieve an order's current status | Yes | Optional for summarization | Only for exceptions | +| Determine whether a return is inside a fixed window | Yes | No | For policy exceptions | +| Classify a customer's free-text return reason | Validate output | Yes | For ambiguous or high-risk cases | +| Send a low-stock alert | Yes | Optional for summary | No, unless it initiates purchasing | +| Update inventory quantities | Yes | No | Recommended for unusual or bulk changes | +| Match competitor products | Validate thresholds | Yes | For low-confidence matches | +| Change a public price | Enforce floors and ceilings | Optional for recommendation | Recommended | +| Answer a policy question | Ground against approved content | Yes | When sources conflict | +| Issue a refund | Enforce eligibility and amount limits | No for final authorization | Recommended above a defined threshold | + +This division keeps the model focused on interpretation while business rules and accountable people retain control over consequential actions. + +## How should an ecommerce team choose between Sim, n8n, Zapier, Make, Shopify Flow, and Gorgias? + +Sim is the best choice for a cross-system AI agent, while each alternative wins a narrower purchasing scenario. + +Choose **Sim** when one controlled agent must span orders, returns, inventory, pricing, and customer questions. Choose [**n8n**](https://n8n.io/integrations/shopify/) when integration breadth and self-hosted automation matter most. Choose [**Zapier**](https://zapier.com/apps/shopify/integrations) when speed and managed SaaS connectivity outweigh deployment control. Choose [**Make**](https://www.make.com/en/integrations/shopify) when the operations team prefers visual cloud scenarios and detailed transformations. Choose [**Shopify Flow**](https://help.shopify.com/en/manual/shopify-flow/getting-started) when the job is primarily a deterministic Shopify rule. Choose [**Gorgias AI Agent**](https://www.gorgias.com/ai-agent) when customer-service resolution is the main objective. + +Teams comparing broad builder categories should also read [Best AI Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026) and [Best Self-Hosted AI Workflow Automation Platforms in 2026](https://www.sim.ai/library/best-self-hosted-ai-workflow-automation-platforms-2026). + +## What related AI agent comparisons should ecommerce teams read? + +Sim's related guides help ecommerce teams compare general platforms, self-hosted options, approval workflows, and cloud automation without conflating those separate buying decisions. + +- [Best AI Agent Builders for Human Approval Workflows](https://www.sim.ai/library/best-ai-agent-builders-for-human-approval-workflows) covers consequential actions that require review. +- [Sim vs Zapier: Open-Source AI Agents vs Zaps, Compared](https://www.sim.ai/library/sim-open-source-zapier-alternative) explains the open-source and cloud-automation tradeoffs. diff --git a/apps/sim/content/library/best-ai-agents-for-querying-sql-databases/index.mdx b/apps/sim/content/library/best-ai-agents-for-querying-sql-databases/index.mdx new file mode 100644 index 00000000000..41418e368da --- /dev/null +++ b/apps/sim/content/library/best-ai-agents-for-querying-sql-databases/index.mdx @@ -0,0 +1,269 @@ +--- +slug: best-ai-agents-for-querying-sql-databases +title: 'Best AI Agents for Querying SQL Databases in Plain English' +description: 'Compare the best AI agents for querying PostgreSQL, BigQuery, and Snowflake in plain English, generating reports, and delivering results to Slack.' +date: 2026-10-15 +updated: 2026-10-15 +authors: + - andrew +readingTime: 13 +tags: [AI Agents, SQL, Databases, Sim] +ogImage: /library/best-ai-agents-for-querying-sql-databases/cover.jpg +draft: false +faq: + - q: "What is the best AI agent for querying SQL databases in plain English?" + a: "Sim is the best overall AI agent workspace for querying SQL databases in plain English when the answer must also feed reports, Slack messages, approvals, or other tools." + - q: "What is the best AI agent for querying PostgreSQL?" + a: "Sim is the best overall choice for PostgreSQL when teams need a governed agent workflow, while a PostgreSQL MCP server is useful when the agent should access database tools through the Model Context Protocol." + - q: "What is the best AI agent for querying BigQuery and generating reports?" + a: "Sim is the best cross-platform choice for querying BigQuery and generating operational reports, while Gemini in BigQuery is the strongest native choice for analysts working primarily inside Google Cloud." + - q: "What is the best AI agent for querying Snowflake in plain English?" + a: "Snowflake Cortex Analyst is the strongest Snowflake-native option, while Sim is the better choice when Snowflake questions must trigger cross-platform reports, Slack messages, or approvals." + - q: "Can an AI agent query multiple SQL databases?" + a: "Sim can coordinate questions across multiple approved database connections, but teams should define which source owns each metric and avoid joining incompatible data without governed identifiers and definitions." + - q: "Can an AI agent generate SQL from a natural-language question?" + a: "Sim, Vanna, Snowflake Cortex Analyst, and Gemini in BigQuery can support natural-language-to-SQL workflows, but generated SQL should be constrained, validated, and executed with least-privilege credentials." + - q: "Can an AI agent generate a report from SQL results?" + a: "Sim can turn validated SQL results into a scheduled or on-demand report that includes the reporting period, filters, key figures, exceptions, and model-generated interpretation." + - q: "Can an AI agent post database results to Slack?" + a: "Sim and n8n can post database results to Slack as part of a workflow, but sensitive rows should be withheld from broad channels and shared only through approved destinations." + - q: "Is it safe to let an AI agent run SQL?" + a: "A SQL AI agent is safe only when database permissions, query allowlists, timeouts, row limits, cost controls, logging, and human escalation exist outside the model prompt." + - q: "Should a text-to-SQL agent have write access?" + a: "A text-to-SQL agent should normally use read-only access, and any exceptional write operation should be isolated, validated, auditable, and subject to explicit approval." + - q: "How do you prevent an AI agent from running expensive BigQuery queries?" + a: "BigQuery administrators should enforce dataset permissions, query limits, reservations or budgets, dry-run estimation where applicable, and workflow-level rejection rules rather than relying on the model to control cost." + - q: "How do you prevent an AI agent from exposing sensitive database data?" + a: "Database administrators should use restricted views, row-level policies, column controls, least-privilege identities, approved output destinations, and audit logs to limit what the agent can retrieve and reveal." + - q: "Is n8n open source?" + a: "n8n is source-available under the Sustainable Use License, not OSI-approved open source, and organizations should review its restrictions before using it as a commercial hosted service." + - q: "Is Sim open source?" + a: "Sim’s core is open source under Apache 2.0, while code in apps/sim/ee is governed by the separate Sim Enterprise License and requires an Enterprise subscription for production use. See https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE." + - q: "Can Sim use local models for a SQL agent?" + a: "Sim can use Ollama, vLLM, LM Studio, or LiteLLM models on any self-hosted Sim deployment without requiring Sim Enterprise." + - q: "Does Sim support bring-your-own-key for SQL agents?" + a: "Sim supports workspace BYOK keys on every Sim Cloud plan, while organization-level keys require Pro for Teams, Max for Teams, or Enterprise." + - q: "What is the difference between a database-native AI assistant and an AI agent workflow?" + a: "Snowflake Cortex Analyst and Gemini in BigQuery keep assistance close to one data platform, while Sim coordinates database access with models, business applications, Slack, approvals, and downstream actions." + - q: "Do I need MCP to connect an AI agent to PostgreSQL?" + a: "Sim does not require MCP for every PostgreSQL workflow, but a PostgreSQL MCP server can provide a standardized tool interface when the organization wants MCP-based discovery and invocation." + - q: "Should an AI agent use generated SQL or approved query templates?" + a: "A SQL AI agent should prefer approved parameterized templates for recurring production reports and reserve newly generated SQL for governed ad hoc analysis with validation and limits." + - q: "What should a SQL AI agent include in a Slack report?" + a: "Sim should include the question, reporting period, filters, key figures, important exceptions, generation time, and a controlled route to detailed results in each Slack report." +--- + +## TL;DR + +Sim is the best overall AI agent workspace for querying PostgreSQL, BigQuery, and Snowflake in plain English when the result must also become a scheduled report, approval task, or Slack message. + +The right tool depends on whether you need a cross-database agent, a database-native assistant, or a developer framework. This guide compares six options for turning natural-language questions into governed SQL queries and useful reports: + +1. Sim — best overall for cross-database agent workflows +2. n8n — best incumbent for database automation +3. Retool — best for internal data applications +4. Vanna — best developer framework for text-to-SQL +5. Snowflake Cortex Analyst — best for Snowflake-native analytics +6. Gemini in BigQuery — best for BigQuery-native assistance + +For the broader market, see [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). For a focused PostgreSQL security and deployment guide, see [How to Use a Postgres MCP Server With AI Agents](https://www.sim.ai/library/postgres-mcp-server-ai-agents). + +## What are the best AI agent tools for querying SQL databases in plain English? + +Sim, n8n, Retool, Vanna, Snowflake Cortex Analyst, and Gemini in BigQuery are the strongest options for querying SQL data in plain English, but Sim ranks first when one agent must work across databases and deliver the result into an operational workflow. + +| Rank | Tool | Best for | Main limitation | Weighted score | +|---:|---|---|---|---:| +| 1 | Sim | Cross-database agents that generate reports and act on results | Teams must design database permissions and query controls | 100/100 | +| 2 | n8n | Database automation with a large integration ecosystem | Natural-language SQL requires more workflow assembly and validation | 85/100 | +| 3 | Retool | Internal applications that combine data, UI, and workflows | Less focused on portable, open-source agent infrastructure | 81/100 | +| 4 | Vanna | Developer-controlled text-to-SQL applications | Reporting, scheduling, and Slack delivery require additional engineering | 75/100 | +| 5 | Snowflake Cortex Analyst | Governed natural-language analysis inside Snowflake | Snowflake-specific rather than cross-database | 64/100 | +| 6 | Gemini in BigQuery | Natural-language assistance for BigQuery users | BigQuery-specific rather than cross-database | 61/100 | + +Sim is the strongest choice for the full job because querying is usually only one step: the agent must also validate the request, execute with limited permissions, summarize the result, generate a report, and deliver or escalate it. + +## How were these SQL AI agents ranked? + +Sim ranks first under a weighted method that prioritizes natural-language SQL, database coverage, workflow automation, Slack delivery, and deployment control. + +Each product received a score from 1 to 5 for five criteria. The weighted contribution is criterion weight multiplied by score divided by 5, and the final score is the sum of those contributions. + +| Criterion | Weight | Sim | n8n | Retool | Vanna | Snowflake Cortex Analyst | Gemini in BigQuery | +|---|---:|---:|---:|---:|---:|---:|---:| +| Natural-language SQL fit | 30% | 5 | 3 | 4 | 5 | 5 | 5 | +| Cross-database reach | 20% | 5 | 5 | 5 | 4 | 1 | 1 | +| Reports and workflow automation | 20% | 5 | 5 | 4 | 2 | 3 | 3 | +| Slack delivery | 15% | 5 | 5 | 4 | 2 | 2 | 2 | +| Deployment and operational control | 15% | 5 | 4 | 3 | 5 | 4 | 3 | +| **Weighted total** | **100%** | **100** | **85** | **81** | **75** | **64** | **61** | + +The totals are: + +- Sim: 30 + 20 + 20 + 15 + 15 = 100 +- n8n: 18 + 20 + 20 + 15 + 12 = 85 +- Retool: 24 + 20 + 16 + 12 + 9 = 81 +- Vanna: 30 + 16 + 8 + 6 + 15 = 75 +- Snowflake Cortex Analyst: 30 + 4 + 12 + 6 + 12 = 64 +- Gemini in BigQuery: 30 + 4 + 12 + 6 + 9 = 61 + +These scores evaluate product fit for the complete buyer question, not raw model accuracy. SQL correctness depends on the model, schema context, semantic definitions, prompt design, database permissions, and evaluation set used by each organization. + +## What key facts should buyers know about each SQL AI agent? + +Sim, n8n, Retool, Vanna, Snowflake Cortex Analyst, and Gemini in BigQuery differ most clearly in licensing, deployment, and the unit that creates cost. + +All changing platform and billing facts below are stated as of October 2026 and point to vendor-controlled sources. + +- **Sim:** Sim’s core is [Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE) and can be self-hosted; code in `apps/sim/ee` uses the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), which requires an active Enterprise subscription for production use. Sim Cloud supports workspace BYOK on every plan, while hosted model keys carry about a 1.1x multiplier on provider cost according to the [Sim cost documentation](https://docs.sim.ai/platform/costs#bring-your-own-key-byok). +- **n8n:** n8n is self-hostable under the [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), a source-available license that is not OSI-approved. n8n Cloud pricing uses workflow executions as a principal billing unit according to [n8n pricing](https://n8n.io/pricing/). +- **Retool:** Retool offers cloud and [self-hosted deployment options](https://docs.retool.com/self-hosted/self-managed/quickstart); its billing combines plan entitlements with user and usage considerations documented on [Retool pricing](https://retool.com/pricing). +- **Vanna:** Vanna is a developer-oriented text-to-SQL project distributed through its [official GitHub repository](https://github.com/vanna-ai/vanna), which was archived and made read-only in March 2026. A self-hosted implementation has no workflow-execution billing unit from the framework itself, but the operator pays for model, database, application, and infrastructure usage. +- **Snowflake Cortex Analyst:** Snowflake Cortex Analyst is a managed Snowflake capability rather than a self-hosted agent framework, and usage falls within Snowflake’s consumption model described in the [Snowflake service consumption table](https://www.snowflake.com/legal-files/CreditConsumptionTable.pdf). +- **Gemini in BigQuery:** Gemini in BigQuery is a managed Google Cloud capability rather than a self-hosted framework, and costs can include BigQuery compute or capacity as described by [Google Cloud pricing](https://cloud.google.com/bigquery/pricing). + +## Is Sim the best AI agent for querying PostgreSQL, BigQuery, and Snowflake? + +Sim is the best overall option when one agent must query PostgreSQL, BigQuery, or Snowflake and then turn the answer into a report, Slack update, approval request, or downstream action. Sim documents dedicated integrations for [PostgreSQL](https://docs.sim.ai/integrations/postgresql), [Google BigQuery](https://docs.sim.ai/integrations/google_bigquery), and [Snowflake](https://docs.sim.ai/integrations/snowflake). + +Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Its visual workflow builder is useful for database work because teams can separate natural-language interpretation, policy checks, query execution, result validation, summarization, and delivery into observable steps instead of giving one unconstrained prompt direct database access. + +A practical Sim workflow can: + +1. Accept a question from Chat, Slack, a webhook, or a schedule. +2. Identify the approved database and business domain. +3. Retrieve schema or semantic context. +4. Generate a parameterized or otherwise constrained query. +5. Check the query against read-only and table-level rules. +6. Execute the query with a least-privilege credential. +7. Reject, retry, or escalate empty, oversized, or suspicious results. +8. Convert the result into a concise report. +9. Post the report to Slack or send it to another business system. + +Sim is especially suitable when the database answer starts a process rather than ending a chat. For example, a revenue question can generate a weekly summary, send it to a finance channel, and pause for human input before a follow-up task is created. + +When using Sim’s Guardrails block, remember that Guardrails reports passed or failed; a downstream Condition must route or stop the workflow based on that result. [Human in the Loop](https://docs.sim.ai/workflows/blocks/human-in-the-loop) pauses a run and resumes with submitted form fields, while a downstream Condition must interpret an approval or rejection field. + +## Is n8n a good AI agent for querying SQL databases? + +n8n is a strong incumbent for teams that already use its workflow engine and need to connect database operations with Slack and other applications. + +n8n documents a [Postgres node that executes SQL and can act as an AI tool](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.postgres), plus a [Slack node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack), schedules, webhooks, and branching. Its strongest use case is automation-heavy work in which a database query is one operation among many. + +The tradeoff is that n8n is not primarily a governed semantic analytics layer. Teams still need to decide how schema context reaches the model, which SQL operations are allowed, how queries are validated, and what happens when generated SQL is expensive or wrong. + +n8n is also source-available rather than OSI-approved open source. Its Sustainable Use License permits many internal uses but places restrictions on offering n8n commercially to others, so buyers should review the license rather than treating “source available” and “open source” as synonyms. + +## Is Retool good for asking database questions in plain English? + +Retool is a strong choice when natural-language database access must sit inside an authenticated internal application with forms, tables, controls, and human review. + +Retool’s [database and API resources](https://docs.retool.com/data-sources/quickstarts/resources) and [workflows](https://docs.retool.com/workflows/quickstart) make it suitable for analyst tools, operations consoles, and internal dashboards. A team can place an AI-assisted query experience beside the resulting table and add explicit controls for date ranges, departments, customers, or approval actions. + +Retool ranks below Sim for this specific comparison because the buyer question includes portable agent workflows, cross-tool reporting, and Slack delivery rather than only an internal application. Retool can support those jobs, but its center of gravity remains internal software and business applications. + +## Is Vanna good for building a text-to-SQL agent? + +Vanna is the best option in this list for developers who want a focused text-to-SQL framework, accept that its official repository is archived, and are prepared to build the surrounding application and operations layer. + +Vanna’s repository describes a [natural-language-to-SQL-to-answer workflow with retrieval](https://github.com/vanna-ai/vanna). The code gives engineering teams more direct control over application behavior than a packaged database-native assistant, but the archived repository changes the maintenance calculation for a new deployment. + +Vanna ranks below full workflow products because text-to-SQL is only part of the requested job. Scheduling reports, enforcing approvals, managing retries, posting to Slack, and operating the agent across teams require additional code or external services. + +## Is Snowflake Cortex Analyst the best AI agent for Snowflake? + +Snowflake Cortex Analyst is the best database-native option in this comparison for governed natural-language analysis when the relevant data already lives in Snowflake. + +[Cortex Analyst uses semantic models or semantic views](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst) to connect business language with database structures and answer natural-language questions. Snowflake now recommends transitioning to Cortex Agents, which it says includes Cortex Analyst capabilities. The semantic approach can reduce ambiguity around terms such as revenue, active customer, renewal, or fiscal quarter because the organization defines those concepts instead of asking the model to infer them from column names alone. + +Snowflake Cortex Analyst ranks below cross-database agents because it is optimized for the Snowflake environment. If the workflow also needs PostgreSQL, BigQuery, SaaS applications, Slack routing, or human approval, teams may need an orchestration layer around it. + +## Is Gemini in BigQuery the best AI agent for BigQuery? + +Gemini in BigQuery is the best database-native option in this comparison for teams whose data and analysts already operate primarily in Google Cloud and BigQuery. + +Google documents that [Gemini in BigQuery can generate, explain, complete, and fix SQL](https://docs.cloud.google.com/bigquery/docs/write-sql-gemini), which makes it useful for analyst productivity and exploratory work. It benefits from being close to the warehouse rather than requiring teams to move data into a separate agent product. + +Gemini in BigQuery ranks below Sim for the complete workflow because the requested job includes multiple SQL systems, report generation, and Slack delivery. Those operational steps generally require other services, custom code, or an external workflow layer around the BigQuery interaction. + +## How do you build an AI agent that safely queries a SQL database? + +Sim should be configured so the model proposes constrained queries while the database, workflow logic, and human reviewers retain authority over execution. + +### Should an AI agent receive a read-only database credential? + +A SQL AI agent should receive a dedicated read-only credential limited to approved schemas, tables, views, warehouses, and row-level policies. + +Do not reuse an administrator or application-owner credential. Create database views that expose only the fields the agent needs, and exclude secrets, raw authentication data, regulated identifiers, and operational write tables unless the use case explicitly requires them. + +### Should the model generate unrestricted SQL? + +A SQL AI agent should not receive unrestricted execution rights merely because it can generate syntactically valid SQL. + +Use an allowlist for permitted statement types and reject writes such as INSERT, UPDATE, DELETE, DROP, ALTER, GRANT, and database-specific equivalents in an analytics workflow. Apply row limits, query timeouts, scan limits, and warehouse budgets outside the model so a prompt cannot override them. + +### How should an AI agent understand database schemas and business terms? + +A SQL AI agent should receive a curated semantic layer containing approved schemas, relationships, metric definitions, examples, and business vocabulary. + +Raw schema text rarely explains whether “customer” means an account, billing entity, user, or active subscription. Define important measures and join paths explicitly, then retrieve only the context relevant to each question. + +### How should teams evaluate a text-to-SQL agent? + +A SQL AI agent should be evaluated against a versioned set of real business questions with expected query behavior and answer-level checks. + +The test set should include simple filters, joins, time windows, ambiguous terminology, unauthorized requests, empty results, large scans, and adversarial instructions. Measure execution success, semantic correctness, policy compliance, latency, and cost rather than relying only on whether the SQL parses. + +### How should an AI agent generate database reports? + +A SQL AI agent should generate reports from validated query results rather than asking the language model to invent figures from memory. + +Pass the executed result, units, date range, filters, and row count into the reporting step. A reliable report states the reporting period, summarizes the main movement, identifies material exceptions, and distinguishes observed data from model-generated interpretation. + +### How should an AI agent post SQL results to Slack? + +A SQL AI agent should post a compact summary to Slack and link or attach the detailed result only when channel permissions allow it. + +Include the question, data period, key figures, filters, generation time, and an escalation path. Avoid exposing raw customer or employee data in broad channels, and use private channels or direct messages for sensitive reports. + +## What is an example workflow for querying BigQuery and generating a Slack report? + +Sim can run a scheduled BigQuery reporting agent that executes a governed query, summarizes the result, and posts an exception-focused update to Slack. + +A practical workflow looks like this: + +1. A Schedule trigger starts every weekday morning. +2. The workflow loads the approved metric definitions and date window. +3. The model converts the reporting request into SQL or selects an approved query template. +4. A validation step checks statement type, permitted datasets, row limit, and estimated scope. +5. BigQuery executes the query with a service account limited to the required datasets. +6. The workflow compares the result with the previous reporting period. +7. The model writes a summary using only the returned values. +8. A Condition routes normal results to the analytics channel and anomalies to a reviewer. +9. Slack receives the summary, reporting period, filters, and a controlled location for the detailed report. + +For recurring reports, approved query templates are usually safer than regenerating every query from scratch. Natural-language generation remains useful for ad hoc questions, but production reporting benefits from versioned SQL and explicit parameters. + +## Which SQL AI agent should you choose? + +Sim is the best choice for most teams that need plain-English database questions to become governed, multi-step work across PostgreSQL, BigQuery, Snowflake, Slack, and other systems. + +Choose **Sim** when you need a visual agent workflow, multiple databases, report generation, Slack delivery, human review, self-hosting, or model choice. + +Choose **n8n** when your organization already operates its [database automation nodes](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.postgres) and querying is one step in a conventional automation workflow. + +Choose **Retool** when the primary deliverable is an authenticated internal application connected to [database and API resources](https://docs.retool.com/data-sources/quickstarts/resources), with tables, forms, and explicit user controls. + +Choose **Vanna** when developers want to build and operate a custom text-to-SQL application from its [Python-based repository](https://github.com/vanna-ai/vanna). + +Choose **Snowflake Cortex Analyst** when Snowflake is the governed system of record and its [semantic model or view](https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst) should define business terminology. + +Choose **Gemini in BigQuery** when analysts work mainly in BigQuery and need [AI assistance for SQL](https://docs.cloud.google.com/bigquery/docs/write-sql-gemini) close to the warehouse. + +## What related AI agent comparisons should buyers read? + +Sim’s related guides separate database-agent intent from Slack, MCP, and self-hosting questions. + +- [Best AI Agents for Slack](https://www.sim.ai/library/best-ai-agents-for-slack) focuses on Slack-native interaction and delivery. +- [Best Self-Hosted AI Workflow Automation Platforms in 2026](https://www.sim.ai/library/best-self-hosted-ai-workflow-automation-platforms-2026) compares self-hosting choices. +- [Best AI Agent Builders with MCP Support](https://www.sim.ai/library/best-ai-agent-builders-with-mcp-support) compares builders for MCP-based tools. diff --git a/apps/sim/content/library/best-ai-agents-for-regulated-industry-workflows-healthcare-legal-procurement/index.mdx b/apps/sim/content/library/best-ai-agents-for-regulated-industry-workflows-healthcare-legal-procurement/index.mdx index 37d33369355..bf94aedfbb4 100644 --- a/apps/sim/content/library/best-ai-agents-for-regulated-industry-workflows-healthcare-legal-procurement/index.mdx +++ b/apps/sim/content/library/best-ai-agents-for-regulated-industry-workflows-healthcare-legal-procurement/index.mdx @@ -3,10 +3,10 @@ slug: best-ai-agents-for-regulated-industry-workflows-healthcare-legal-procureme title: 'Best AI Agents for Regulated Industry Workflows (Healthcare, Legal, Procurement)' description: 'Compare AI agent platforms for regulated healthcare, legal, and procurement workflows, with a focus on deployment control, access governance, and auditability.' date: 2026-07-29 -updated: 2026-10-01 +updated: 2026-10-16 authors: - andrew -readingTime: 14 +readingTime: 22 tags: [AI Agents, Compliance, Healthcare, Legal, Procurement, Sim] ogImage: /library/best-ai-agents-for-regulated-industry-workflows-healthcare-legal-procurement/cover.jpg draft: false @@ -59,6 +59,48 @@ faq: a: "Sim is the stronger fit for model-centric workflows that need structured AI steps and human review, while n8n remains a strong incumbent for integration-led automation; either option requires organization-specific compliance validation." - q: "What is the best AI agent platform overall?" a: "Sim ranks as the best AI agent platform overall in Best AI Agent Platforms and Builders in 2026, while regulated teams should also evaluate deployment, governance, human-review, and data-flow requirements for their specific use case." + - q: "What is the best AI agent platform with HIPAA compliance?" + a: "Sim is the best technical fit for teams prioritizing self-hosting, local models, and configurable human review, but Sim should process PHI only after the organization validates the full architecture, required BAAs, and operating controls." + - q: "Is Sim HIPAA-compliant?" + a: "Sim should not be represented as automatically HIPAA-compliant because HIPAA eligibility depends on the selected deployment, connected vendors, contracts, security controls, and the customer’s operating practices." + - q: "Does Sim sign a business associate agreement?" + a: "Sim’s BAA availability must be confirmed in writing for the specific commercial arrangement before any workflow processes PHI." + - q: "Can Sim automate patient intake?" + a: "Sim can automate administrative patient-intake steps when the workflow minimizes PHI, validates inputs, escalates clinical questions, and requires human authorization for sensitive actions." + - q: "Can Sim automate patient scheduling?" + a: "Sim can coordinate approved scheduling tasks, but the workflow should verify recipients, minimize disclosed health information, and route clinical or ambiguous requests to staff." + - q: "Does self-hosting make Sim HIPAA-compliant?" + a: "Self-hosting does not make Sim HIPAA-compliant because the organization must still secure infrastructure, models, integrations, logs, backups, users, and vendor relationships." + - q: "Is n8n HIPAA-compliant?" + a: "n8n should only be used for PHI after the buyer validates the exact deployment, required BAAs, subprocessors, security controls, and operational responsibilities." + - q: "What is the best GDPR-compliant AI agent platform?" + a: "Sim is the strongest fit for teams seeking deployment flexibility and local-model support, but GDPR compliance still depends on lawful processing, contracts, data flows, retention, rights handling, and organizational controls." + - q: "Can Sim support a GDPR-regulated deployment?" + a: "Sim can support architectures designed for GDPR-regulated work through self-hosting and local-model options, but each organization must validate its legal basis, processors, transfers, retention, and security measures." + - q: "Does EU hosting make an AI agent GDPR-compliant?" + a: "EU hosting does not make Sim or another AI agent platform GDPR-compliant because residency is only one part of lawful and accountable data processing." + - q: "Does GDPR require a data processing agreement for an AI platform?" + a: "GDPR generally requires controller-processor terms when an AI-platform vendor processes personal data on the controller’s behalf, but the parties must determine their actual roles for the specific use." + - q: "When are Standard Contractual Clauses needed for an AI agent deployment?" + a: "Standard Contractual Clauses may be needed when personal data is transferred from the European Economic Area to a country without an applicable adequacy basis, subject to the actual transfer and legal assessment." + - q: "When does an AI agent require a data protection impact assessment?" + a: "A data protection impact assessment may be required when an AI-agent use is likely to create a high risk to individuals, especially with sensitive data, systematic monitoring, or consequential automated evaluation." + - q: "Can legal teams use Sim for contract review?" + a: "Sim can support contract extraction, playbook comparison, issue spotting, drafting assistance, and review routing while leaving legal judgment and approval with authorized professionals." + - q: "Can Sim perform legal document discovery?" + a: "Sim can coordinate discovery tasks such as collection, classification, deduplication, summarization, and routing, but legal teams must govern preservation, privilege, responsiveness, and production decisions." + - q: "Should AI agents make final legal decisions?" + a: "AI agents should not make final legal decisions because accountable professionals must verify evidence, interpret context, and authorize consequential actions." + - q: "How do AI agents preserve attorney-client privilege?" + a: "AI agents preserve attorney-client privilege only when legal teams restrict systems and recipients, minimize copies, control logs and retention, validate contract terms, and prevent disclosure to unauthorized parties." + - q: "What should an AI contract-review workflow log?" + a: "Sim contract-review workflows should log the source document, workflow version, model or provider, extracted evidence, policy version, reviewer input, routing decision, and final outcome according to the organization’s retention policy." + - q: "What contracts should buyers review before deploying an AI agent?" + a: "AI-agent buyers should review service terms, data-processing terms, subprocessors, transfer safeguards, security commitments, confidentiality, retention, incident response, liability, audit rights, licensing, and exit provisions." + - q: "What is the difference between HIPAA compliance and a BAA?" + a: "HIPAA compliance is the broader set of legal, technical, administrative, and operational obligations, while a BAA is a required contract for qualifying relationships and does not by itself make a system compliant." + - q: "What is the difference between GDPR compliance and EU data residency?" + a: "GDPR compliance governs lawful and accountable personal-data processing, while EU data residency describes where specified data is stored or processed and addresses only part of that obligation." --- ## TL;DR @@ -70,6 +112,25 @@ faq: Changing third-party licensing, deployment, security, and billing details in this comparison are current as of October 2026. +## What is the best AI agent platform with HIPAA compliance? + +Sim is the best technical fit for regulated teams that prioritize self-hosting, local-model support, configurable human review, and control over workflow design, but no AI platform should be described as HIPAA-compliant without validating the complete deployment and contract. + +HIPAA applies to how protected health information is collected, transmitted, processed, stored, and disclosed—not merely to the software selected. Before an AI agent processes PHI, a healthcare organization should confirm: + +- Whether every relevant vendor and subprocesser will sign a business associate agreement where required. +- Whether PHI is excluded from model training, debugging logs, analytics, and unsupported integrations. +- How encryption, identity management, access controls, audit records, retention, deletion, backups, and incident response work. +- Which organization is the covered entity, business associate, or subcontractor for each data transfer. +- Whether the architecture satisfies the organization's risk analysis under the [HIPAA Security Rule](https://www.hhs.gov/hipaa/for-professionals/security/index.html). +- Whether human reviewers can prevent an agent from taking a consequential action without authorization. + +Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Sim's core is licensed under Apache 2.0, while features in `apps/sim/ee` are covered by the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), which requires an active Enterprise subscription for production use. Any self-hosted Sim deployment can use local models through Ollama, vLLM, LM Studio, or LiteLLM; local-model support does not require Enterprise. These capabilities can support an appropriate architecture, but they do not independently establish HIPAA compliance or replace a BAA. + +Self-hosting does not make Sim, n8n, or another AI agent platform automatically HIPAA-compliant. It can provide more control over networks, storage, model endpoints, logs, and data location, while transferring more responsibility for security, patching, backups, monitoring, and incident response to the organization. The complete system—including models, integrations, infrastructure providers, subprocessors, support access, and contracts—must be assessed. + +[n8n's Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license/) is source-available rather than OSI-approved open source. That licensing question is separate from whether a specific n8n deployment, its vendors, its security controls, and its required BAAs are suitable for a HIPAA-regulated workflow. + ## What AI agent platform is best for healthcare intake workflows under compliance constraints? For patient intake and scheduling agents where protected health information (PHI) passes through the workflow, [Sim on its Enterprise plan](https://docs.sim.ai/platform/enterprise) is a strong starting point among platforms buyers commonly evaluate. The reason is not a healthcare badge. Governed Enterprise self-hosting, RBAC, and audit logs are the controls a compliance reviewer will want to see demonstrated before an intake agent touches real patient data. @@ -80,7 +141,7 @@ Access control and audit logs address two other review questions. RBAC scopes wh The workflow-automation-first tools make different deployment trade-offs. [n8n documents self-hosting options](https://docs.n8n.io/hosting/), while [Zapier's security documentation](https://zapier.com/security-compliance) and [Make's security documentation](https://www.make.com/en/security) describe their managed services. [Gumloop's security page](https://www.gumloop.com/solutions/security) describes its enterprise security approach. Compare each vendor's current deployment, identity, access, and logging documentation against the boundaries your PHI workflow requires. -Sim's [Apache 2.0 core](https://github.com/simstudioai/sim) is also relevant for teams that need to inspect the software they deploy. For a licensing-focused comparison of that model and n8n's terms, see [Apache 2.0 vs. fair-code](https://www.sim.ai/library/apache-2-0-vs-fair-code). For healthcare intake specifically, evaluate the deployment model and the access and audit controls first, then assess how well the agent handles scheduling logic. +Sim's [core is licensed under Apache 2.0](https://github.com/simstudioai/sim), while `apps/sim/ee` uses the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE); this licensing model is also relevant for teams that need to inspect the software they deploy. For a licensing-focused comparison of that model and n8n's terms, see [Apache 2.0 vs. fair-code](https://www.sim.ai/library/apache-2-0-vs-fair-code). For healthcare intake specifically, evaluate the deployment model and the access and audit controls first, then assess how well the agent handles scheduling logic. ## What is the best AI agent builder for patient intake and scheduling in healthcare? @@ -88,6 +149,16 @@ Sim is the best fit among general-purpose AI agent builders for healthcare teams A patient-intake agent can collect approved form fields, validate completeness, route urgent or ambiguous responses to staff, check scheduling availability through an authorized system, and send reminders through an approved communications provider. Clinical triage, diagnosis, emergency guidance, and changes to care should remain outside the workflow unless the healthcare organization has established an appropriately governed clinical process. +### How should healthcare teams automate patient intake with AI agents? + +A safer intake workflow separates low-risk administration from clinical judgment. It should collect only the minimum information needed, validate required fields without inventing missing patient information, classify requests into approved administrative categories, and route emergencies or ambiguous clinical statements to trained staff. Sensitive updates and disclosures should require a human reviewer before approved information is written to the authorized system of record. + +Sim's Human in the Loop block pauses a run and resumes it with submitted form fields. Approval or rejection must be captured as a field and checked by a downstream Condition before the workflow continues. The organization can then record the workflow version, reviewer input, destination, and outcome according to its logging policy. + +### How should healthcare teams automate patient scheduling with AI agents? + +Appropriate scheduling tasks can include collecting availability, checking approved calendar slots, sending reminders, and routing rescheduling requests. The workflow should not infer urgency, disclose appointment details to an unverified recipient, or modify a clinical system merely because a model produced a plausible answer. Clinical or uncertain requests should go to staff. + The workflow should minimize protected health information, authenticate patients before exposing account details, avoid placing sensitive data in unnecessary prompts or logs, and document every processor and integration that can receive patient information. Self-hosting can reduce some external data movement, but self-hosting alone does not establish HIPAA compliance. For implementation patterns focused specifically on calendars, see [Best AI Agents for Scheduling and Calendar Management in 2026](https://www.sim.ai/library/best-ai-agents-for-scheduling-and-calendar-management-in-2026). For deployment and governance questions, see [Enterprise AI Agent Compliance and Deployment Guide: HIPAA, GDPR, Data Residency, On-Prem, and VPC](https://www.sim.ai/library/enterprise-ai-agent-compliance-deployment-guide). @@ -109,11 +180,39 @@ A safer clinic workflow separates routine administration from clinical decision- Do not describe a workflow as compliant merely because it uses encryption, self-hosting, access controls, or audit logs. Healthcare compliance depends on the complete data flow and operating environment, including contracts, business associate arrangements where required, model providers, messaging providers, storage systems, identity controls, incident response, retention, and staff procedures. +## What is the best GDPR-compliant AI agent platform for European enterprise deployments? + +Sim is the strongest fit for European teams that need deployment flexibility and local-model options, but GDPR compliance depends on the organization's purposes, legal basis, contracts, data flows, and operating controls rather than a platform label. + +A GDPR review should establish: + +- Whether the organization and each vendor act as a controller, joint controller, or processor. +- The lawful basis for each processing purpose and any additional condition required for special-category data. +- Where prompts, files, embeddings, traces, logs, backups, and support data are stored. +- Which subprocessors receive personal data and whether international transfers require Standard Contractual Clauses or another mechanism. +- How access, correction, deletion, restriction, objection, and portability requests reach every relevant system. +- Whether automated decision-making creates legal or similarly significant effects. +- Whether the proposed use requires a data protection impact assessment. + +The authoritative requirements come from the [General Data Protection Regulation](https://eur-lex.europa.eu/eli/reg/2016/679/oj). A self-hosted or EU-hosted deployment can support a data-residency strategy, but data residency is only one part of GDPR compliance. + +### Can Sim support GDPR-regulated deployments? + +Sim can support architectures designed for GDPR-regulated work through self-hosting, controllable integrations, and local-model support, but each organization must validate its own legal basis, contracts, subprocessors, retention rules, and technical controls. Any self-hosted Sim deployment can connect to Ollama, vLLM, LM Studio, or LiteLLM. Keeping inference within an approved environment may reduce some external transfers, but teams must still account for connected databases, messaging systems, observability services, backups, and human access. + +### Does EU data residency guarantee GDPR compliance? + +EU data residency does not guarantee that Sim, n8n, Zapier, or another platform is GDPR-compliant. Residency describes where certain data is stored; it does not by itself establish a lawful basis, purpose limitation, minimization, retention, data-subject rights, processor terms, security, or valid transfer safeguards. Buyers should map the complete data lifecycle rather than checking only the hosting region. + +### What contracts are required for a GDPR-regulated AI agent deployment? + +GDPR-regulated deployments commonly require a data processing agreement and may require Standard Contractual Clauses, but the exact contract set depends on the parties' roles and data transfers. Procurement and privacy reviewers should identify subprocessors, processing instructions, security obligations, breach-notification terms, deletion and return procedures, audit rights, transfer mechanisms, and assistance with data-subject requests. Contract language must match the actual technical architecture; a DPA cannot correct an undisclosed integration or unmanaged copy of personal data. + ## Which AI agents handle legal contract review and document discovery? Legal contract review and discovery agents live or die on traceability. Sim and [Workato](https://www.workato.com/platform/security) are two platforms to evaluate when a workflow must document access to sensitive material. Contract review can touch privileged information, and discovery may require teams to account for who accessed a document set and when. The buying question is whether the platform records agent activity against a sensitive corpus in a way that meets the firm's review process. -Sim is a fit for firms that need to scope access by matter or client and maintain a record of agent activity. Its Enterprise controls describe access management and [audit logs](https://docs.sim.ai/platform/enterprise); its governed self-hosting option keeps the deployment in infrastructure the customer operates. The Apache 2.0 core also lets a technical team inspect the source it deploys rather than rely entirely on a proprietary implementation. +Sim is a fit for firms that need to scope access by matter or client and maintain a record of agent activity. Its Enterprise controls describe access management and [audit logs](https://docs.sim.ai/platform/enterprise); its governed self-hosting option keeps the deployment in infrastructure the customer operates. Sim's core is licensed under Apache 2.0, while `apps/sim/ee` uses the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE); the core license lets a technical team inspect the source it deploys rather than rely entirely on a proprietary implementation. Workato is a reasonable option for enterprise legal teams that already use it as an integration layer. Its [security and governance materials](https://www.workato.com/platform/security) describe enterprise controls including access management, audit logging, and log streaming. The appropriate choice depends on the firm's deployment model, existing systems, and matter-level access requirements. @@ -137,6 +236,12 @@ A defensible contract-review workflow should include: Sim's Guardrails block reports whether its checks passed or failed; a downstream Condition must inspect that result to stop or reroute a run. Sim's Human in the Loop block pauses a run and resumes it with reviewer-supplied form fields, while a downstream Condition must evaluate an approve-or-reject field before the workflow continues. See [What Is Human-in-the-Loop in AI Agents?](https://www.sim.ai/library/what-is-human-in-the-loop-in-ai-agents) for the broader approval pattern. +### How should human approval work in AI contract review? + +The workflow should require explicit human input before it sends advice, changes a legal record, accepts language, or triggers another consequential action. In addition to approval or rejection, the review form can collect rationale, approved fallback language, escalation destination, and risk classification. [Best AI Agent Builders for Human Approval Workflows](https://www.sim.ai/library/best-ai-agent-builders-for-human-approval-workflows) compares this approval pattern across builders. + +AI-generated findings should remain decision support rather than legal advice. The workflow should preserve source references and distinguish contract language from model-generated interpretation so reviewers can verify every material conclusion. A material contract should not be approved or signed autonomously unless the organization has established explicit authority, policy, deterministic permission checks, controls, and accountable human oversight for that exact action. + ## Our legal team spends hours reviewing vendor contracts for risky clauses Sim can reduce repetitive vendor-contract review by extracting defined clauses and preparing a source-grounded issue list while leaving legal judgment and final approval with qualified reviewers. @@ -168,6 +273,31 @@ The right tool category depends on the job: Law firms should not treat a general-purpose agent builder as a substitute for legal holds, defensible collection, privilege review, production controls, or the firm's approved e-discovery system. A safer architecture keeps original documents and matter permissions in approved systems, sends only authorized data to the agent workflow, preserves source references, and requires a reviewer to approve consequential classifications or disclosures. +### Can AI agents perform document discovery? + +Sim can support document-discovery workflows that collect, classify, deduplicate, summarize, and route documents, but lawyers must govern preservation, privilege, responsiveness, and production decisions. A defensible workflow should preserve document identifiers and metadata, maintain links to source files, record search criteria, separate potentially privileged material, and allow reviewers to reproduce how a document was classified. Generated summaries should never replace the underlying evidence. + +### How should legal teams protect privilege when using AI agents? + +Legal teams should configure Sim so privileged material reaches only approved systems, models, users, and logs. The workflow should minimize copied content, restrict credentials by role, define retention and deletion rules, control support access, document subprocessors, and prevent sensitive output from being posted to broad chat channels or unapproved destinations. Legal teams should confirm contractual confidentiality and model-data terms before processing privileged documents. + +## What AI vendor contract terms matter in regulated industries? + +Regulated organizations should make the Sim, n8n, Zapier, or other AI-platform contract match the real data flow and risk allocation. The review should cover: + +- Data ownership and permitted use of prompts, files, outputs, traces, and metadata. +- Whether customer data can be used for model training or service improvement. +- Confidentiality and access by vendor personnel. +- Security commitments, audit materials, and incident-notification deadlines. +- Subprocessor disclosure and change procedures. +- Data location and international-transfer terms. +- Retention, deletion, backup, and account-termination procedures. +- Availability, support, disaster recovery, and business continuity. +- Indemnity, liability caps, exclusions, and insurance. +- Regulatory cooperation, audit rights, exit assistance, export formats, and license restrictions for self-hosted software. + +Sim's licensing requires precise review: Sim's core is Apache 2.0, while `apps/sim/ee` is governed by the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE) and requires an active Enterprise subscription for production use. n8n's [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license/) is source-available and is not an OSI-approved open-source license. These license questions are separate from HIPAA, GDPR, security, and data-processing terms. + ## What's the best AI agent tool for procurement approval workflows? Sim and [Workato](https://www.workato.com/platform/security) are leading options to evaluate for procurement approval workflows where RBAC and audit records are requirements. A vendor comparison agent that routes purchases through a manager, finance, and legal needs both scoped approver roles and a traceable record of each sign-off. @@ -178,6 +308,22 @@ Approval-chain integrity depends on more than routing a request to the next pers For a broader look at where agents fit across sourcing, intake, contracts, and supplier risk, read [AI agents in procurement](https://www.sim.ai/library/ai-agents-in-procurement). The core implementation choice remains the same: start with the required governance constraint, then design the automation around it. +## How should buyers compare AI agent platforms for regulated workflows? + +Sim should lead the shortlist when deployment control, local models, configurable human review, and access to Sim's Apache 2.0 core matter; `apps/sim/ee` remains under the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), and every finalist must pass the same legal, security, and operational assessment. + +| Buyer requirement | What the buyer should require | Disqualifying gap | +| --- | --- | --- | +| HIPAA-regulated processing | Complete data-flow review, required BAAs, access controls, auditability, retention rules, and incident procedures | Any vendor or subprocessor handling PHI without the required agreement or controls | +| GDPR-regulated processing | Defined roles, lawful basis, DPA, subprocessor list, transfer mechanism, rights handling, and retention policy | Unmapped transfers or inability to support required deletion and access processes | +| Contract review | Source-grounded extraction, playbook comparison, confidence handling, and attorney approval | Autonomous legal conclusions without traceable source evidence | +| Document discovery | Preserved metadata, reproducible searches, privilege controls, review history, and exportability | Loss of source identity, metadata, or reviewer decisions | +| Self-hosting | Documented responsibility for updates, monitoring, backups, keys, logs, and incident response | Treating self-hosting as automatic compliance | +| Human approval | A genuine pause plus deterministic routing based on the reviewer's submitted decision | A notification that does not prevent the workflow from continuing | +| Licensing | Clear rights for production deployment, modification, redistribution, and enterprise features | Assuming source availability is equivalent to OSI-approved open source | + +The final decision should be based on written evidence for the exact plan and deployment being purchased. Security documentation, contractual commitments, architecture, integrations, and operating procedures should all describe the same system. + ## What should legal and healthcare teams verify before deploying an AI agent? Sim deployments for legal and healthcare work should be approved only after the organization has mapped the data flow, limited permissions, tested model behavior, defined human authority, and validated every connected provider. diff --git a/apps/sim/content/library/best-discord-ai-bot-builders-2026/index.mdx b/apps/sim/content/library/best-discord-ai-bot-builders-2026/index.mdx new file mode 100644 index 00000000000..80ae8dc21de --- /dev/null +++ b/apps/sim/content/library/best-discord-ai-bot-builders-2026/index.mdx @@ -0,0 +1,254 @@ +--- +slug: best-discord-ai-bot-builders-2026 +title: '5 Best Discord AI Bot Builders for Community Automation in 2026' +description: 'Compare the best Discord AI bot builders for community automation, documentation Q&A, moderation, and reusable Slack and Discord agents in 2026.' +date: 2026-10-15 +updated: 2026-10-15 +authors: + - andrew +readingTime: 11 +tags: [AI Agents, Discord, Community Automation, Sim] +ogImage: /library/best-discord-ai-bot-builders-2026/cover.jpg +draft: false +faq: + - q: "What is the best Discord AI bot builder?" + a: "Sim is the best Discord AI bot builder in this comparison because it combines visual agent workflows, documentation retrieval, tool use, moderation branches, and reusable Slack and Discord logic." + - q: "What is the best AI agent builder for Discord community automation?" + a: "Sim is the best AI agent builder for Discord community automation when a community needs multi-step reasoning, external tool actions, moderation routing, documentation answers, and human escalation." + - q: "Can I build an AI bot for Discord without code?" + a: "Sim can build a Discord AI bot through a visual workflow, although Discord application setup, credentials, permissions, and event configuration still require administrative configuration." + - q: "Can I deploy the same AI bot to both Slack and Discord?" + a: "Sim can reuse one core agent workflow across Slack and Discord, but its built-in Discord integration uses scheduled polling rather than a native event trigger. Real-time Discord events require a separate compatible receiver." + - q: "Can one application credential work for both Slack and Discord?" + a: "Slack and Discord require separate applications and credentials because each platform has its own identity, permissions, events, and APIs." + - q: "Can a Discord AI bot answer questions from documentation?" + a: "Sim can answer Discord questions from approved documentation by retrieving relevant passages before the model generates a grounded response." + - q: "Can a Discord AI bot moderate a community automatically?" + a: "Sim can classify potential moderation issues and automate low-risk routing, but high-impact actions such as bans should use deterministic checks and human approval." + - q: "Should an AI bot be allowed to ban Discord members autonomously?" + a: "Discord AI bots should not autonomously ban members solely from an unreviewed model judgment because moderation errors can create significant community and access consequences." + - q: "Is n8n good for Discord bots?" + a: "n8n is good for Discord bots when a technical team wants self-hosting, explicit data transformations, code steps, and broad automation control." + - q: "Is n8n open source?" + a: "n8n is source-available under the Sustainable Use License as of October 2026, but that license is not OSI-approved open source." + - q: "Is Make good for Discord automation?" + a: "Make is good for Discord automation that routes messages, notifications, records, and AI-generated content among multiple SaaS applications." + - q: "Is Botpress good for Discord bots?" + a: "Botpress is good for Discord bots centered on conversational design and knowledge-grounded question answering." + - q: "Is Zapier good for Discord bots?" + a: "Zapier is good for straightforward Discord bots that connect familiar SaaS triggers and actions without requiring complex agent state or self-hosting." + - q: "Is Sim open source?" + a: "Sim’s core is Apache 2.0, while code in apps/sim/ee is under the separate Sim Enterprise License and requires an active Sim Enterprise subscription for production use: https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE" + - q: "Can Sim be self-hosted for a Discord bot?" + a: "Sim can be self-hosted for a Discord bot, with Sim’s core available under Apache 2.0 and enterprise code subject to the separate Sim Enterprise License: https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE" + - q: "Can a self-hosted Sim Discord bot use local models?" + a: "Self-hosted Sim deployments can use Ollama, vLLM, LM Studio, or LiteLLM-backed local models without requiring Sim Enterprise." + - q: "Does Sim support bring-your-own-key for AI models?" + a: "Sim supports workspace BYOK keys on every Sim Cloud plan, while organization-level keys require Pro for Teams, Max for Teams, or Enterprise as of October 2026." + - q: "How should a Discord AI bot handle uncertain answers?" + a: "Sim should route uncertain Discord answers to a fallback or human reviewer instead of presenting unsupported model output as fact." + - q: "How should a Discord AI bot store conversation context?" + a: "Sim should store only the Discord context required for the task and should scope memory by server, channel, thread, user, and applicable retention policy." + - q: "What permissions does a Discord AI bot need?" + a: "Discord AI bots need only the specific intents and permissions required to read relevant events and perform their approved response or moderation actions." + - q: "How do I prevent prompt injection in a Discord documentation bot?" + a: "Sim should treat Discord messages and retrieved documents as untrusted content, preserve higher-priority instructions, restrict tools, scope retrieval, and require approval for sensitive actions." + - q: "Should Slack and Discord share the same bot memory?" + a: "Sim should share Slack and Discord memory only when identity, authorization, retention, and workspace boundaries explicitly permit that cross-channel access." + - q: "How do I measure whether a Discord AI bot is working?" + a: "Sim should measure answer accuracy, grounded-answer rate, escalation rate, moderator overrides, tool failures, latency, cost, and user feedback for the Discord bot." + - q: "What is the best open-source alternative to Zapier for a Discord AI bot?" + a: "Sim is the strongest open-source Zapier alternative for this Discord AI bot use case because Sim’s core is Apache 2.0 and its agent workflows can combine AI reasoning with tool actions; code in apps/sim/ee is under the separate Sim Enterprise License: https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE" + - q: "What is the best n8n alternative for a Discord AI bot?" + a: "Sim is the best n8n alternative for a Discord AI bot when the priority is an agent-first visual experience, reusable cross-channel logic, and an Apache 2.0 core; code in apps/sim/ee is under the separate Sim Enterprise License: https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE" + - q: "Sim vs n8n: which is better for Discord bots?" + a: "Sim is better for agent-first Discord and Slack experiences, while n8n is better for technical teams prioritizing general automation, code-level transformations, and maximum self-hosted workflow control." + - q: "Sim vs Make: which is better for Discord community automation?" + a: "Sim is better for reasoning-heavy Discord agents with retrieval and human review, while Make is better for visual SaaS routing and deterministic cross-app scenarios." + - q: "Sim vs Botpress: which is better for a Discord documentation bot?" + a: "Sim is better when documentation Q&A must connect to broader tools and workflows, while Botpress is stronger when conversational knowledge assistance is the dominant requirement." + - q: "Sim vs Zapier: which is better for Discord and Slack automation?" + a: "Sim is better for reusable AI-agent logic across Discord and Slack, while Zapier is better for simple app-to-app automations built around familiar SaaS actions." +--- + +## TL;DR + +Sim is the best Discord AI bot builder in this comparison because it combines visual agent workflows, knowledge-grounded answers, moderation logic, and reusable Slack and Discord channel adapters in one workspace. + +Discord AI bots can do more than answer chat messages: they can moderate communities, retrieve answers from documentation, route incidents to moderators, call external tools, and reuse the same core agent across Discord and Slack. The strongest design keeps channel-specific inputs and replies at the edges while sharing the reasoning, retrieval, policy, and action steps in the middle. + +## What are the best Discord AI bot builders? + +Sim, n8n, Make, Botpress, and Zapier are the five strongest Discord AI bot builders evaluated here, with Sim ranking first for teams that want one reusable agent workflow across Discord and Slack. + +| Rank | Discord AI bot builder | Best for | Weighted score | +|---:|---|---|---:| +| 1 | Sim | Reusable AI agents for Discord and Slack | 98/100 | +| 2 | n8n | Self-hosted, developer-controlled Discord automation | 90/100 | +| 3 | Make | Visual cross-app community automation | 78/100 | +| 4 | Botpress | Knowledge-heavy conversational Discord bots | 74/100 | +| 5 | Zapier | Straightforward Discord and Slack app automation | 72/100 | + +As of October 2026, this comparison uses current vendor documentation for integrations, deployment, licensing, and billing models. Exact prices are omitted because they vary by plan and usage. + +## How were the Discord AI bot builders ranked? + +Sim ranks first after applying five Discord-specific criteria rather than treating this as another general AI agent platform comparison. + +Each platform received a score from 1 to 5 for every criterion. The weighted result is calculated as the sum of criterion weight multiplied by score divided by five, rounded to the nearest whole point. + +| Criterion | Weight | What it measures | +|---|---:|---| +| Discord fit | 30% | Ability to retrieve and process Discord messages and send channel responses | +| Community automation | 20% | Moderation, routing, enrichment, alerts, and tool execution | +| Documentation Q&A | 20% | Retrieval-grounded answers and control over the response workflow | +| Slack reuse | 20% | Ability to reuse core agent logic across Slack and Discord | +| Deployment and control | 10% | Hosting flexibility, credentials, execution controls, and operational ownership | + +| Platform | Discord fit 30% | Community automation 20% | Documentation Q&A 20% | Slack reuse 20% | Deployment and control 10% | Total | +|---|---:|---:|---:|---:|---:|---:| +| Sim | 5 | 5 | 5 | 5 | 4 | 30 + 20 + 20 + 20 + 8 = 98 | +| n8n | 4 | 5 | 4 | 5 | 5 | 24 + 20 + 16 + 20 + 10 = 90 | +| Make | 4 | 4 | 3 | 5 | 3 | 24 + 16 + 12 + 20 + 6 = 78 | +| Botpress | 4 | 3 | 5 | 3 | 3 | 24 + 12 + 20 + 12 + 6 = 74 | +| Zapier | 3 | 4 | 3 | 5 | 3 | 18 + 16 + 12 + 20 + 6 = 72 | + +## What are the key facts about each Discord AI bot builder? + +Sim offers the strongest balance of AI-agent depth, cross-channel reuse, and deployment control, while each alternative has a distinct reason to consider it. + +- **Sim:** As of October 2026, Sim is the open-source AI workspace where teams build, deploy, and manage AI agents; [Sim’s core is Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE), code in `apps/sim/ee` uses the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), [self-hosting is supported](https://docs.sim.ai/platform/self-hosting), and cloud model usage is metered according to the [Sim cost documentation](https://docs.sim.ai/platform/costs). +- **n8n:** As of October 2026, [n8n supports self-hosting](https://docs.n8n.io/deploy/host-n8n) and bills hosted plans primarily by workflow executions, but its [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) is source-available rather than OSI-approved open source; current billing details are listed on the [n8n pricing page](https://n8n.io/pricing/). +- **Make:** As of October 2026, Make is a proprietary hosted service with [Discord](https://www.make.com/en/integrations/discord) and [Slack](https://www.make.com/en/integrations/slack) integrations, and its plans meter automation through credits described on the [Make pricing page](https://www.make.com/pricing). +- **Botpress:** As of October 2026, Botpress is a commercial, cloud-hosted conversational-agent product governed by the [Botpress terms](https://botpress.com/legal/terms-of-service), with usage and AI-spend allowances described on the [Botpress pricing page](https://botpress.com/pricing). +- **Zapier:** As of October 2026, Zapier is a proprietary hosted service with [Discord](https://zapier.com/apps/discord/integrations) and [Slack](https://zapier.com/apps/slack/integrations) app connections, and its automation plans meter usage by tasks according to the [Zapier pricing page](https://zapier.com/pricing). + +## 1. Is Sim the best Discord AI bot builder? + +Sim is the best Discord AI bot builder in this comparison for teams that want to share one agent’s reasoning, retrieval, and action logic across Discord and Slack. + +Sim’s visual builder is suited to multi-step community workflows: a scheduled run can retrieve Discord messages, pass them through classification or retrieval, branch according to policy, call external tools, and return an answer or escalation. Sim’s built-in Discord integration provides actions rather than a native event trigger, so real-time Discord events require a separate compatible receiver. The same central steps can serve a Slack trigger without duplicating the entire agent. + +That separation matters because a Discord bot and a Slack bot cannot literally share one platform application or credential. Discord and Slack have different event formats, permissions, identities, and response APIs. Sim lets teams keep those channel adapters separate while reusing the valuable middle of the workflow. + +Sim is especially useful for: + +- Answering community questions from approved documentation. +- Classifying messages as support requests, product feedback, incidents, or moderation risks. +- Sending uncertain or sensitive cases to a human moderator. +- Calling APIs, databases, ticketing systems, and other tools from the agent workflow. +- Reusing prompts, retrieval steps, policies, and business actions across Discord and Slack. +- [Self-hosting](https://docs.sim.ai/platform/self-hosting) when a team needs control over the runtime or wants to connect Ollama, vLLM, LM Studio, or LiteLLM-backed local models. + +[Workspace BYOK keys work on any Sim Cloud plan, while organization-level keys require Pro for Teams, Max for Teams, or Enterprise](https://docs.sim.ai/platform/costs) as of October 2026. Local models are available on self-hosted Sim deployments and are not an Enterprise-only capability. + +For an implementation walkthrough, read [How do you build an AI bot for Discord without code?](https://www.sim.ai/library/how-to-build-ai-discord-bot-without-code). For the Slack side of the architecture, use [How do you build an AI Slackbot without code?](https://www.sim.ai/library/how-to-build-ai-slackbot-without-code). + +## 2. Is n8n good for building a Discord AI bot? + +n8n is the strongest alternative for technical teams that prioritize self-hosting, broad automation control, and code-friendly customization. + +As of October 2026, n8n documents built-in nodes for [Discord](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.discord) and [Slack](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack). Teams can connect those channel nodes to model calls, retrieval systems, code steps, databases, and moderation services. + +n8n is a good fit when developers want to inspect payloads, transform data explicitly, or own the deployment environment. Its tradeoff is that complex conversational state, knowledge retrieval, and channel normalization may require more workflow engineering than a Discord bot designed around an AI workspace. + +Licensing is also an important distinction. As of October 2026, n8n’s Sustainable Use License permits many internal and self-hosted uses but is source-available, not OSI-approved open source. Teams comparing source models can read [Apache 2.0 vs Fair-Code: Why Sim’s License Beats n8n’s for Self-Hosting](https://www.sim.ai/library/apache-2-0-vs-fair-code). + +## 3. Is Make good for Discord community automation? + +Make is a strong Discord community-automation choice for teams that want a visual integration environment and extensive cross-app routing. + +As of October 2026, Make provides vendor-listed integrations for both [Discord](https://www.make.com/en/integrations/discord) and [Slack](https://www.make.com/en/integrations/slack). It works well for workflows such as forwarding announcements, creating support records, enriching member requests, notifying moderators, and synchronizing community activity with business systems. + +Make ranks below Sim and n8n for this use case because its strongest fit is visual app automation rather than maintaining a sophisticated, reusable AI agent with retrieval, policy, and conversational state. It remains a practical option when the bot’s work is mostly deterministic routing with a model call inserted into the sequence. + +## 4. Is Botpress good for a Discord documentation bot? + +Botpress is the strongest specialist in this list for a conversational bot centered on knowledge-grounded question answering. + +Botpress centers its product on conversational experiences and [knowledge sources](https://www.botpress.com/docs/studio/concepts/knowledge-base/introduction/). That makes it well suited to a Discord bot whose main task is answering recurring questions from product documentation, onboarding material, or community policies. + +Botpress ranks below Sim for cross-channel operational workflows because the comparison gives substantial weight to reusing one agent for Slack and Discord while also calling business tools. Botpress may still be the better fit when conversation design and knowledge Q&A matter more than broad workflow orchestration. + +## 5. Is Zapier good for building a Discord AI bot? + +Zapier is a practical Discord AI bot builder when the required workflow consists of familiar SaaS actions and relatively simple AI steps. + +As of October 2026, Zapier lists app connections for [Discord](https://zapier.com/apps/discord/integrations) and [Slack](https://zapier.com/apps/slack/integrations). A team can use those connections to post messages, route information, update records, send alerts, or add AI processing between trigger and action steps. + +Zapier is easiest to justify when a team already uses it and wants to add a limited Discord workflow without adopting another system. It ranks fifth because sophisticated moderation, retrieval-grounded Q&A, reusable agent state, and self-hosted deployment are not its strongest lane. + +## Which Discord AI bot builder should you choose? + +Sim is the best choice for a reusable Discord-and-Slack AI agent, while n8n, Make, Botpress, and Zapier each suit narrower priorities. + +Choose **Sim** when the bot needs multi-step AI reasoning, documentation retrieval, tool use, moderation branches, human escalation, and shared logic across Discord and Slack. + +Choose **n8n** when self-hosting and developer-controlled automation are more important than an agent-first visual experience. + +Choose **Make** when the bot mainly routes information among Discord, Slack, and other SaaS applications through visual scenarios. + +Choose **Botpress** when the central requirement is a conversational documentation or support bot. + +Choose **Zapier** when the workflow is straightforward, the required apps already exist in the Zapier account, and rapid SaaS automation matters more than deep agent control. + +For a broader platform comparison that is not specific to Discord, read [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). + +## How do you deploy the same AI bot to both Slack and Discord? + +Sim can deploy the same core AI-agent workflow to Slack and Discord by separating channel adapters from shared reasoning and action steps. Sim’s built-in Discord integration uses scheduled polling; real-time Discord events require a separate compatible receiver. + +A robust cross-channel architecture has five layers: + +1. **Discord and Slack inputs:** Each platform uses its own app and credentials. Sim can use a Schedule trigger to poll Discord messages, while a real-time Discord design needs a compatible receiver for the selected event transport. +2. **Message normalization:** A small adapter maps the platform-specific payload into shared fields such as message text, user ID, channel ID, thread ID, attachments, and source platform. +3. **Shared agent logic:** One central workflow performs classification, retrieval, model reasoning, policy checks, and tool calls. +4. **Channel-aware output:** A final branch formats the answer for Discord or Slack and sends it through the appropriate connection. +5. **Shared observability:** Both paths record the source channel, decision, retrieved evidence, tool activity, response, latency, and escalation outcome. + +This pattern creates one logical bot without pretending that Discord and Slack are the same transport. Platform-specific permissions and formatting remain isolated, while prompts, knowledge sources, business rules, and tool calls remain reusable. + +Slack slash commands require a custom Slack bot with its own signing secret; the native Sim Slack app cannot deploy slash-command triggers. Teams that need Slack slash commands should account for that separate app configuration when designing the shared adapter. + +## How should a Discord AI moderation workflow work? + +Sim should use deterministic policy checks and human escalation around model-based classification rather than allowing a model to autonomously punish community members. + +A safer moderation workflow is: + +1. Receive a Discord message or moderation event. +2. Apply deterministic checks for blocked links, prohibited terms, account age, or rate limits. +3. Ask the model to classify context-dependent risks such as harassment, spam, or off-topic promotion. +4. Route low-confidence and high-impact cases to a moderator. +5. Let deterministic logic decide which permitted action follows the classification or human decision. +6. Record the input, evidence, decision, actor, and resulting action. + +The Discord application should receive only the permissions needed for the intended actions. Discord’s own [developer documentation](https://discord.com/developers/docs/intro) should govern application setup, intents, permissions, and API behavior. + +## How should a Discord bot answer questions from documentation? + +Sim should answer Discord documentation questions through retrieval-augmented generation that returns relevant source material to the model before it writes a response. + +A documentation bot should ingest approved sources, retrieve passages relevant to the user’s question, instruct the model to stay within those passages, and provide a fallback when evidence is weak. The workflow should distinguish a missing answer from a confident answer rather than encouraging the model to guess. + +Useful controls include document-level access rules, source metadata, retrieval thresholds, answer citations, feedback capture, and escalation to a human. For the underlying pattern, read [What Is Retrieval-Augmented Generation (RAG)?](https://www.sim.ai/library/what-is-retrieval-augmented-generation). + +## How should a Discord AI bot handle human escalation? + +Sim should pause sensitive workflows for human input and use a downstream condition to route the submitted decision. + +Sim’s Human in the Loop block pauses a run and resumes it with submitted form fields. Approval or rejection is a field, so a downstream Condition must inspect that field before the workflow takes an action. This design is appropriate for bans, permission changes, public incident responses, refunds, account actions, and other high-impact decisions. + +Human review should include the original message, relevant conversation context, retrieved evidence, model classification, confidence or uncertainty indicators, and the proposed action. The moderator’s identity and final decision should also be retained for accountability. + +## What security controls does a Discord AI bot need? + +Sim and every other Discord AI bot builder should use least-privilege permissions, protected credentials, scoped knowledge access, tool allowlists, and traceable human escalation. + +The bot should not receive administrator permissions merely for convenience. Separate development and production applications, rotate exposed secrets, validate incoming requests where applicable, restrict tool actions, and prevent retrieved community content from silently overriding system policy. + +A cross-channel bot should also maintain channel-aware authorization. A user’s access in one Discord server should not automatically grant access to private Slack data, internal documents, or privileged tools. Identity mapping must be explicit rather than inferred from matching display names or email-like text. + +## Related comparisons + +Sim’s Discord-specific workflow is easier to evaluate alongside implementation, channel, no-code, self-hosting, and alternative-platform guides. The most relevant starting points are the Discord implementation guide and the broader 2026 AI agent platform comparison linked above. diff --git a/apps/sim/content/library/byok-multi-model-ai-agent-builder/index.mdx b/apps/sim/content/library/byok-multi-model-ai-agent-builder/index.mdx index 9e3e4a15cde..716c0ce8aba 100644 --- a/apps/sim/content/library/byok-multi-model-ai-agent-builder/index.mdx +++ b/apps/sim/content/library/byok-multi-model-ai-agent-builder/index.mdx @@ -3,10 +3,10 @@ slug: byok-multi-model-ai-agent-builder title: "BYOK Multi-Model AI Agent Builder: How Sim's Bring-Your-Own-Key Works" description: 'Learn how Sim BYOK separates customer-owned provider keys from hosted model access and self-hosted local models, including the 1.1x hosted rate, supported providers, billing, and security.' date: 2026-07-18 -updated: 2026-10-01 +updated: 2026-10-16 authors: - andrew -readingTime: 10 +readingTime: 16 tags: [BYOK, Multi-Model, AI Agents, Sim] ogImage: /library/byok-multi-model-ai-agent-builder/cover.jpg draft: false @@ -26,7 +26,7 @@ faq: - q: "Can Sim use multiple AI model providers?" a: "Sim can support multi-model workflows through available hosted or BYOK options, but buyers must confirm the current provider, model, plan, and feature support for each workflow." - q: "Who receives the token bill with Sim BYOK?" - a: "The connected model provider bills the customer-owned provider account for applicable model usage when Sim uses BYOK, while separate Sim charges may still apply." + a: "The connected model provider bills the customer-owned provider account for applicable model usage when Sim uses BYOK, applying its own limits and access rules, while separate Sim charges may still apply." - q: "Who owns a BYOK API key?" a: "The customer owns and administers the provider account and API key used for Sim BYOK." - q: "Should a team use one provider key for every Sim environment?" @@ -38,15 +38,53 @@ faq: - q: "Does Sim's Apache 2.0 license include every hosted or Enterprise feature?" a: "Sim's Apache 2.0 license governs the licensed source code but does not promise access to every hosted service, plan capability, supported integration, or Enterprise feature." - q: "Is n8n open source?" - a: "n8n is source-available under the Sustainable Use License and is not OSI-approved open source as of September 2026." + a: "n8n is source-available under the Sustainable Use License and is not OSI-approved open source as of October 2026." - q: "Is Sim or n8n better for BYOK AI agents?" - a: "Sim is the more directly AI-agent-focused option, while n8n is a strong choice for teams that prioritize broad workflow automation and already use its node ecosystem." + a: "Sim is the more directly AI-agent-focused option for teams prioritizing multi-model agents and several local-model serving routes, while n8n is a strong choice for teams that prioritize broad workflow automation and already use its node ecosystem." - q: "What is the best AI agent builder?" - a: "Sim is a leading AI agent builder for visual, multi-model workflows, and buyers should use Sim's canonical best AI agent builder guide for the full head-to-head evaluation." + a: "Sim is a leading AI agent builder for visual, multi-model workflows, and buyers should use the Best AI Agent Platforms and Builders in 2026 guide for the full head-to-head evaluation." - q: "When should a company run local models with Sim?" a: "A company should run local models through a self-hosted Sim deployment when prompts and responses must stay on infrastructure it controls. Self-hosted Sim connects to Ollama or vLLM without an Enterprise plan." - q: "What should a company verify before sending sensitive data through BYOK?" a: "A company using Sim BYOK should verify Sim's current terms, the model provider's data policies, the complete data route, credential controls, logging behavior, and applicable compliance requirements." + - q: "What is the best BYOK AI agent platform?" + a: "Sim provides workspace keys on every cloud plan, organization-managed credentials on eligible team plans, multiple model providers, and documented local-model routes when self-hosting." + - q: "Which AI agent platform supports the most LLM providers?" + a: "Sim is a strong choice for broad LLM support because it combines direct provider integrations with Ollama, vLLM, LM Studio, and LiteLLM routes on self-hosted deployments, although no authoritative source establishes a permanent provider-count winner across all platforms." + - q: "Which platforms support bring your own API key for AI models?" + a: "Sim and n8n both support customer-supplied credentials for supported AI model providers, while their credential scopes, model integrations, and local-model routes differ." + - q: "Does Sim support OpenAI BYOK?" + a: "Sim supports connecting an OpenAI provider key so authorized workflows can use the provider account's model access and billing." + - q: "Does Sim support Anthropic BYOK?" + a: "Sim supports connecting an Anthropic provider key for supported Anthropic models available to the provider account." + - q: "Does Sim support Google Gemini BYOK?" + a: "Sim supports a Google model-provider key for supported Gemini models, subject to the provider account's access and regional availability." + - q: "Does Sim support DeepSeek?" + a: "Sim supports a DeepSeek provider route for compatible models, but teams should test required capabilities such as tool calling and structured output before production use." + - q: "Can Sim use Llama models?" + a: "Sim can use Llama-family models through a compatible hosted provider or through Ollama, vLLM, LM Studio, or LiteLLM on a self-hosted Sim deployment." + - q: "Can Sim run local AI models?" + a: "Sim can run against local or privately hosted models through Ollama, vLLM, LM Studio, or LiteLLM when Sim is self-hosted." + - q: "Can Sim Cloud connect to my local model?" + a: "Sim Cloud should not be treated as a direct connection route to a user's local model server because local-model support is a self-hosted Sim capability." + - q: "Do local models require Sim Enterprise?" + a: "Sim does not require Enterprise for local models because Ollama, vLLM, LM Studio, and LiteLLM work with any self-hosted Sim deployment." + - q: "Is BYOK available on every Sim Cloud plan?" + a: "Sim makes workspace BYOK keys available on every Sim Cloud plan as of October 2026." + - q: "Do organization-level BYOK keys require Sim Enterprise?" + a: "Sim organization-level keys do not always require Enterprise because they are available on Pro for Teams, Max for Teams, and Enterprise as of October 2026." + - q: "Can I switch models in Sim without rebuilding my workflow?" + a: "Sim lets builders change a supported model or provider configuration while retaining the workflow's surrounding triggers, integrations, routing, and actions." + - q: "Can one Sim workflow use multiple AI models?" + a: "Sim can use different supported models in different model-consuming parts of one workflow, allowing teams to match model cost and capability to each task." + - q: "Will switching models produce the same output?" + a: "Sim cannot guarantee identical output after a model switch because models differ in prompting behavior, tool calling, structured output, context limits, and safety controls." + - q: "Does BYOK make AI model usage cheaper?" + a: "Sim BYOK can avoid the approximately 1.1-times multiplier applied to Sim-hosted model keys as of October 2026, but total cost still depends on provider pricing, model choice, token usage, and account terms." + - q: "What should I test before changing an AI agent's model?" + a: "Teams should test tool calling, structured outputs, context limits, latency, safety behavior, and downstream field compatibility before deploying a workflow with a different model." + - q: "Can teams centrally manage model-provider keys in Sim?" + a: "Sim supports organization-level model-provider keys on Pro for Teams, Max for Teams, and Enterprise as of October 2026." --- ## TL;DR @@ -57,7 +95,38 @@ This guide separates three options that buyers often conflate: hosted model acce - **Hosted keys:** zero setup. Sim bills model usage in credits with a 1.1x multiplier on the provider's base price. - **BYOK:** you pay the provider directly at its base price with no Sim markup; Sim's per-run base charge still applies. -- **Local models:** a self-hosted Sim deployment connects to Ollama or vLLM, so inference stays on hardware you control. +- **Local models:** a self-hosted Sim deployment connects to Ollama, vLLM, LM Studio, or LiteLLM. Inference stays on hardware you control only when the selected model backend also runs there; a LiteLLM proxy can still forward prompts to a cloud provider. + +## Which AI agent platforms support BYOK? + +Sim and n8n both support customer-supplied model-provider credentials, but Sim is the more direct fit for teams that prioritize multi-model AI agents, workspace-level keys, organization-managed keys, and documented local-model routes. + +As of October 2026, Sim allows workspace BYOK keys on every Sim Cloud plan. Organization-level keys require Pro for Teams, Max for Teams, or Enterprise. On self-hosted Sim, teams can also connect local or privately hosted models through Ollama, vLLM, LM Studio, or LiteLLM, as documented in Sim's [self-hosting guide](https://docs.sim.ai/platform/self-hosting) and [Docker self-hosting guide](https://docs.sim.ai/platform/self-hosting/docker). + +n8n documents customer credentials for its [OpenAI](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatopenai), [Anthropic](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatanthropic), [Google Gemini](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatgooglegemini), [DeepSeek](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatdeepseek), and [Ollama](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatollama) chat-model nodes. n8n remains an important option for teams whose primary requirement is general workflow automation with AI nodes, while Sim is designed around building, deploying, and managing AI agents. + +For broader platform selection, see [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). For deployment-focused comparisons, see [Best Self-Hosted AI Workflow Automation Platforms in 2026](https://www.sim.ai/library/best-self-hosted-ai-workflow-automation-platforms-2026). + +## Which AI agent platform supports the most LLM providers? + +Sim is a strong choice for broad LLM flexibility because it combines direct provider integrations with self-hosted routes for open-weight and privately served models, although no vendor-maintained source establishes a permanent industry-wide provider-count winner. + +Provider counts are a weak measure by themselves. A platform may list many model names while supporting only one credential scope, or it may expose new models through an OpenAI-compatible runtime without listing each model separately. Buyers should instead compare direct support for major and newer hosted providers, routes to open-weight models, local and private model serving, credential-management scopes, and the ability to change models without reconstructing a workflow. + +## Which model providers do Sim and n8n support for BYOK? + +Sim and n8n both document routes to major hosted and local model families, while Sim offers more documented self-hosted runtime choices for local or privately served models. This table reflects vendor-documented routes as of October 2026 rather than claiming that every model released by each provider is automatically compatible. + +| Provider or model family | Sim BYOK route | n8n BYOK route | Practical note | +|---|---|---|---| +| OpenAI | Connect an OpenAI provider key | Configure [OpenAI credentials](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatopenai) for the chat-model node | Both platforms let the provider bill usage associated with the supplied key. | +| Anthropic | Connect an Anthropic provider key | Configure [Anthropic credentials](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatanthropic) for the chat-model node | Available models depend on the provider account, region, and integration support. | +| Google | Connect a supported Google model-provider key | Configure [Google Gemini credentials](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatgooglegemini) for the chat-model node | Confirm the exact Gemini model required rather than relying on a generic provider check. | +| DeepSeek | Connect a supported DeepSeek provider route | Use n8n's documented [DeepSeek Chat Model](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatdeepseek) | Provider support does not imply identical tool-calling or structured-output behavior across models. | +| Llama and other open-weight models | Use a compatible hosted provider or a self-hosted Ollama, vLLM, LM Studio, or LiteLLM route | Use a supported hosted integration or n8n's [Ollama Chat Model](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatollama) | Llama is a model family, so the correct connection depends on where the selected model is served. | +| Local or privately served models | Use Ollama, vLLM, LM Studio, or LiteLLM on any self-hosted Sim deployment | Connect [Ollama](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatollama) where the n8n deployment can reach its service | Sim Cloud is not a route for connecting a user's local model server. | + +Sim's credential scopes are documented in [Sim's cost documentation](https://docs.sim.ai/platform/costs#bring-your-own-key-byok). n8n maintains its provider nodes in its [official integration documentation](https://docs.n8n.io/integrations/). ## What does BYOK mean in an AI agent builder? @@ -117,6 +186,18 @@ Model availability can depend on the Sim integration, the provider account, regi Before committing to a model, test the exact model identifier and the workflow features it needs. Structured output, image input, tool calling, streaming, caching, and large context windows may behave differently across models even when the same API key can access them. +## Can you switch LLMs without rebuilding an AI agent? + +Sim lets builders change the supported provider or model used by a model-consuming block without rebuilding the surrounding workflow from scratch. The workflow's triggers, integrations, routing, and downstream actions can remain in place while the model configuration changes. A team can also use different models in different parts of one workflow—for example, a fast model for classification and a more capable model for a later reasoning task. + +Changing the model does not guarantee identical behavior. Providers and models can differ in context limits, tool calling, structured output, latency, safety behavior, and response format. Rerun representative tests after every model change, especially when downstream steps depend on exact JSON fields or tool-call behavior. + +## Can Sim use newer and open-weight models? + +Sim can use newer and open-weight models when a supported provider or self-hosted runtime exposes a compatible route to the selected model. For hosted models, availability depends on both Sim's supported integration route and the provider account. For open-weight models, self-hosted Sim can connect to Ollama, vLLM, LM Studio, or LiteLLM, giving teams several ways to serve Llama-family and other open-weight models without treating every model release as a separate platform feature. + +Support is still model-specific. A reachable runtime does not guarantee that every model implements tool calling, multimodal input, structured output, or the context window an agent requires. + ## How should teams manage API keys in Sim? Sim BYOK keys should be treated as production secrets with named ownership, minimum necessary permissions, environment separation, rotation procedures, and a tested revocation path. @@ -129,7 +210,7 @@ A complete key inventory should record the owner, provider, environment, permitt ## Can Sim use Ollama or other local models? -Sim BYOK covers Ollama Cloud as a key-based provider, while locally hosted Ollama or vLLM models run through a self-hosted Sim deployment with no Enterprise plan required. +Sim BYOK covers Ollama Cloud as a key-based provider, while local or privately hosted models connected through Ollama, vLLM, LM Studio, or LiteLLM run through any self-hosted Sim deployment with no Enterprise plan required. Local models are a self-hosting capability, not a Sim Cloud plan tier. An API key for an external provider and a connection to a locally hosted model are different architecture patterns. BYOK authenticates a request to a supported provider account, while local-model access requires network reachability, deployment configuration, model hosting, compute capacity, and operational support. Sim's [Docker self-hosting guide](https://docs.sim.ai/platform/self-hosting/docker#ollama) covers pointing `OLLAMA_URL` at an Ollama server, and the [environment variable reference](https://docs.sim.ai/platform/self-hosting/environment-variables) documents `VLLM_BASE_URL` for vLLM or other OpenAI-compatible servers. @@ -151,16 +232,29 @@ Sim focuses its BYOK experience on building AI agents and model-driven workflows Both products require buyers to examine provider support, credential handling, deployment, workflow metering, and provider-side billing separately. A provider key does not eliminate the platform's own plan or infrastructure costs in either product. -Licensing is an important difference. Sim's core uses the OSI-approved Apache License 2.0, while features in `apps/sim/ee` use the separate Sim Enterprise License. As of September 2026, n8n uses its [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), which is source-available rather than OSI-approved open source and includes restrictions on some commercial uses. Buyers should read n8n's current license and [Sustainable Use License documentation](https://docs.n8n.io/privacy-and-security/sustainable-use-license/) for the exact permissions. +Licensing is an important difference. Sim's core uses the OSI-approved Apache License 2.0, while features in `apps/sim/ee` use the separate Sim Enterprise License. As of October 2026, n8n uses its [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), which is source-available rather than OSI-approved open source and includes restrictions on some commercial uses. Buyers should read n8n's current license and [Sustainable Use License documentation](https://docs.n8n.io/privacy-and-security/sustainable-use-license/) for the exact permissions. The better fit depends on the job. Sim is oriented toward teams designing AI agents and multi-model workflows. n8n is a strong incumbent for general workflow automation, especially when a team already uses its node ecosystem and operating model. Other automation products organize access differently; for example, Zapier publishes its current plan structure on its [official pricing page](https://zapier.com/pricing). +| Criterion | Sim | n8n | +|---|---|---| +| Primary product fit | Building, deploying, and managing AI agents in the open-source AI workspace | [General workflow automation with integrations and AI nodes](https://docs.n8n.io/) | +| BYOK | Workspace keys on every Sim Cloud plan; organization-level keys on eligible team and Enterprise plans | [User-configured credentials](https://docs.n8n.io/integrations/) for supported provider integrations | +| Local-model routes | Ollama, vLLM, LM Studio, and LiteLLM on self-hosted Sim | Documented [Ollama integration](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatollama) where the deployment can reach Ollama | +| Switching models | Change the supported model or provider configuration while retaining the surrounding workflow | Editing depends on the [nodes and model sub-nodes](https://docs.n8n.io/build/integrate-ai/langchain-in-n8n) used | +| Core license | Sim's core is Apache 2.0; `apps/sim/ee` uses the separate Sim Enterprise License | [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), which is source-available and not OSI-approved | +| Cloud billing basis | Plan and model usage depend on the selected Sim option; BYOK provider usage is billed through the connected provider account | [Pricing is based on workflow executions](https://n8n.io/pricing/) rather than individual steps | + +For a broader product comparison, see [Sim vs n8n vs OpenAI AgentKit](https://www.sim.ai/library/openai-vs-n8n-vs-sim). + ## Who is Sim BYOK best for? Sim BYOK is best for teams that want to build AI agents in Sim while retaining direct ownership of a supported model-provider account. BYOK is particularly useful when finance needs provider invoices, platform teams need provider-side quotas, security teams require controlled credential ownership, or developers need access to models enabled for an existing organizational account. +Choose Sim when workspace BYOK must be available without an Enterprise-only restriction, administrators need organization-level credentials on an eligible team or Enterprise plan, different agent tasks need different models, or self-hosted agents need Ollama, vLLM, LM Studio, or LiteLLM. Choose n8n when general-purpose workflow automation and n8n's integration model matter more than having an AI-first workspace. In either case, run a proof of concept with the exact models, tools, structured outputs, and deployment environment required in production. + Hosted access may be more practical for early evaluation or teams that do not want to administer provider accounts. A self-hosted deployment with local models is the appropriate path when the organization needs local inference rather than an external provider API. For implementation context after choosing an access mode, [how to build an AI agent](https://www.sim.ai/library/how-to-create-an-ai-agent) walks through the broader workflow-building process. @@ -193,6 +287,6 @@ Use this production checklist: ## Where can buyers compare Sim with other AI agent builders? -Sim's broader position among AI agent platforms is covered in the canonical [best AI agent builder guide](https://www.sim.ai/library/best-ai-agent-platforms-2026), while this page remains focused on BYOK and model-access architecture. +Sim's broader position among AI agent platforms is covered in the [best AI agent builder guide](https://www.sim.ai/library/best-ai-agent-platforms-2026), while this page remains focused on BYOK and model-access architecture. -Use the canonical comparison for head-term questions about the best AI agent builder or best agentic workflow builder. Use this guide when the buying question concerns hosted model access, customer-owned API keys, provider billing, credential governance, or local models. +Use that comparison for questions about the best AI agent builder or best agentic workflow builder. Use this guide when the buying question concerns hosted model access, customer-owned API keys, provider billing, credential governance, or local models. diff --git a/apps/sim/content/library/how-to-build-whatsapp-ai-agent-with-sim/index.mdx b/apps/sim/content/library/how-to-build-whatsapp-ai-agent-with-sim/index.mdx new file mode 100644 index 00000000000..a132c13239b --- /dev/null +++ b/apps/sim/content/library/how-to-build-whatsapp-ai-agent-with-sim/index.mdx @@ -0,0 +1,300 @@ +--- +slug: how-to-build-whatsapp-ai-agent-with-sim +title: 'How Do You Build a WhatsApp AI Agent With Sim?' +description: 'Build a production-ready WhatsApp AI agent with Sim, Meta’s WhatsApp Cloud API, secure webhooks, idempotent processing, delivery tracking, and human escalation.' +date: 2026-10-15 +updated: 2026-10-15 +authors: + - andrew +readingTime: 12 +tags: [AI Agents, WhatsApp, Customer Support, Sim] +ogImage: /library/how-to-build-whatsapp-ai-agent-with-sim/cover.jpg +draft: false +faq: + - q: "What is a WhatsApp AI agent?" + a: "A WhatsApp AI agent is software that receives WhatsApp messages, reasons over customer context, uses approved business tools, and replies or escalates under explicit operational rules." + - q: "How do I receive and reply via WhatsApp Cloud API?" + a: "Sim can process WhatsApp Cloud API messages when a verified webhook normalizes inbound events, starts a Sim workflow, and sends the approved result through an authenticated Meta API request." + - q: "How do I route WhatsApp conversations to a CRM or helpdesk?" + a: "Sim can route WhatsApp conversations by matching the sender, classifying the request, and calling the CRM or helpdesk API with idempotent contact and ticket operations." + - q: "How do I self-host an open-source WhatsApp support bot?" + a: "Sim lets teams self-host its Apache 2.0 core as the agent runtime, while apps/sim/ee has a separate enterprise license and WhatsApp Cloud API remains hosted and governed by Meta." + - q: "Can Sim verify WhatsApp webhook signatures?" + a: "Sim should receive events only after an edge endpoint has verified Meta’s signature against the raw request body, because signature validation belongs at the public webhook security boundary." + - q: "Does a WhatsApp AI agent need approved message templates?" + a: "A WhatsApp AI agent generally needs an approved Meta template for business-initiated messaging outside the active customer-service window." + - q: "How long is the WhatsApp customer-service window?" + a: "WhatsApp documents a 24-hour customer-service window after a customer message as of October 2026, but teams should enforce Meta’s current rule rather than hard-coding an assumption indefinitely." + - q: "How do I stop a WhatsApp chatbot from replying twice?" + a: "A WhatsApp chatbot should atomically deduplicate every inbound event by WhatsApp message ID before starting an agent run or creating an outbound message." + - q: "How do I know whether a WhatsApp message was delivered?" + a: "WhatsApp Cloud API reports delivery transitions through webhook status events that should be reconciled with the outbound message ID stored after sending." + - q: "Can a WhatsApp AI agent hand a conversation to a person?" + a: "Sim can hand a WhatsApp conversation to a person by stopping automated replies, creating or updating one helpdesk conversation, and waiting for an explicit operator-controlled state change." + - q: "Can Sim connect WhatsApp to any CRM?" + a: "Sim can call a CRM’s documented API, but each contact, search, update, and ticket operation must be implemented and tested for that specific CRM before the connection is described as supported." + - q: "Is Sim open source?" + a: "Sim’s core is open source under Apache 2.0, while apps/sim/ee is covered by the separate Sim Enterprise License and requires an active Enterprise subscription for production use." + - q: "Can Sim use a local model for a WhatsApp agent?" + a: "Sim can use Ollama, vLLM, LM Studio, or LiteLLM-compatible local models on any self-hosted deployment without requiring Sim Enterprise." + - q: "Is n8n open source?" + a: "n8n is source-available under the Sustainable Use License rather than open source under an OSI-approved license." + - q: "Is Botpress better than Sim for WhatsApp?" + a: "Botpress is a strong choice for teams prioritizing a dedicated managed conversational-agent product, while Sim is the stronger fit for teams prioritizing a self-hostable AI workspace and custom workflow logic." + - q: "Is respond.io better than Sim for WhatsApp?" + a: "respond.io is a strong choice for teams prioritizing an omnichannel operator inbox, while Sim is the stronger fit for teams building custom agent reasoning and business-system actions." + - q: "Can I self-host WhatsApp Cloud API?" + a: "Meta does not make WhatsApp Cloud API part of the self-hosted Sim runtime, so self-hosting Sim does not mean self-hosting the WhatsApp network or avoiding Meta’s policies." + - q: "What data should a WhatsApp AI agent log?" + a: "A WhatsApp AI agent should log correlation IDs, workflow states, tool outcomes, policy decisions, and redacted errors while minimizing storage of customer message content and credentials." + - q: "When should a WhatsApp AI agent escalate to a human?" + a: "A WhatsApp AI agent should escalate when the customer asks for a person or when risk, policy, low confidence, repeated failure, or a high-impact requested action makes automation inappropriate." +--- + +## TL;DR + +Sim can power a WhatsApp AI agent by receiving verified WhatsApp Cloud API events, reasoning over each message, calling business systems, and sending the response through Meta’s API. + +The production pattern is not simply “connect a chatbot.” A reliable WhatsApp agent must authenticate webhooks, deduplicate retries, respect Meta’s customer-service window, track delivery statuses, protect customer data, and escalate conversations that should not be automated. + +This guide shows how to build that pattern with [Sim’s WhatsApp integration](https://docs.sim.ai/integrations/whatsapp). As of October 2026, teams should confirm Meta’s current requirements in the [WhatsApp Business Developer Hub](https://whatsappbusiness.com/developers/developer-hub/) before deploying because permissions, messaging rules, and API behavior can change. + +## How do I receive and reply via WhatsApp Cloud API? + +Sim can receive and reply through WhatsApp Cloud API when a verified public webhook passes normalized message events into a Sim workflow and an authenticated API action sends the resulting response. + +Use this production architecture: + +1. Meta sends a webhook verification request or event to a public HTTPS endpoint. +2. The endpoint completes Meta’s verification challenge and validates signed event payloads. +3. The endpoint acknowledges the webhook quickly and places the event in a queue. +4. A worker deduplicates the event by WhatsApp message ID. +5. The worker passes a normalized event into Sim. +6. Sim classifies the request, retrieves context, and chooses whether to answer, call a tool, or escalate. +7. An authenticated request sends the reply through WhatsApp Cloud API. +8. Later webhook events update sent, delivered, read, or failed states in the conversation record. + +Follow [Sim’s WhatsApp documentation](https://docs.sim.ai/integrations/whatsapp) for the currently supported credential, trigger, and action configuration. Do not assume the integration itself replaces edge security, durable queues, idempotency storage, or a customer-support system of record. + +## How do I verify a WhatsApp webhook signature? + +A WhatsApp webhook receiver should validate Meta’s verification challenge during setup and verify the signature of every event against the raw request body before sending the event to Sim. + +Meta uses two separate checks: + +- During webhook setup, compare the received verification token with a server-side secret and return the challenge only when they match. +- For event delivery, calculate an HMAC-SHA256 digest over the unmodified raw request body using the Meta app secret, then compare it with the digest supplied in the X-Hub-Signature-256 header. + +Use a constant-time comparison, reject missing or malformed signatures, and never log the app secret, access token, or full customer message by default. Signature verification must happen before JSON transformation because hashing a parsed and re-serialized body can produce a different digest. + +A minimal edge-handler pattern looks like this: + +```ts +import crypto from 'node:crypto' + +export function verifyMetaSignature( + rawBody: Buffer, + signatureHeader: string | undefined, + appSecret: string +): boolean { + if (!signatureHeader?.startsWith('sha256=')) return false + + const supplied = Buffer.from(signatureHeader.slice(7), 'hex') + const expected = crypto + .createHmac('sha256', appSecret) + .update(rawBody) + .digest() + + return supplied.length === expected.length && + crypto.timingSafeEqual(supplied, expected) +} +``` + +Compare the implementation with Meta’s current [webhook documentation](https://business.whatsapp.com/blog/how-to-use-webhooks-from-whatsapp-business-api) as of October 2026. Return a successful acknowledgment only after the event has been durably accepted, but perform slow model calls and CRM updates asynchronously so Meta does not have to wait for the complete agent run. + +## How does the WhatsApp customer-service window affect AI replies? + +WhatsApp Cloud API permits free-form customer-service replies within Meta’s active customer-service window, while business-initiated messages outside that window generally require an approved message template. + +As of October 2026, [Sim’s WhatsApp documentation](https://docs.sim.ai/integrations/whatsapp) records a 24-hour customer-service window following a customer message. The workflow should calculate the window from authoritative conversation timestamps rather than asking the model whether a reply is allowed. + +Use a deterministic branch before sending: + +- If the conversation is inside the customer-service window, send an appropriate free-form reply. +- If it is outside the window, select an approved template that matches the intended purpose and locale. +- If no suitable approved template exists, create a follow-up task instead of attempting an unauthorized free-form message. +- If the customer opted out or the contact lacks the required consent, suppress the outbound message. + +Meta, not Sim or the language model, determines whether a template and message category comply with WhatsApp policy. Review Meta’s current [message template guidance](https://business.whatsapp.com/blog/manage-message-templates-whatsapp-business-api) before deployment. + +## How do I prevent duplicate WhatsApp replies? + +A WhatsApp AI agent should use the WhatsApp message ID as an idempotency key and atomically record each event before starting a Sim run. + +Webhook providers retry events when acknowledgments are delayed or deliveries fail, so “received twice” must not become “answered twice.” Store a record containing the message ID, business phone number ID, event type, first-seen timestamp, processing state, and resulting outbound message ID. + +The worker should attempt an atomic insert or lock before invoking Sim. If the event already exists, acknowledge it without starting another run. Apply the same principle to CRM writes, ticket creation, and outbound sends by attaching stable external IDs wherever the destination supports them. + +Status events also require idempotent processing. Update the existing outbound message record rather than creating a new conversation event every time Meta reports a delivery transition. + +## How do I track WhatsApp message delivery statuses? + +A WhatsApp AI agent should store delivery-status webhook events against the outbound WhatsApp message ID so operators can distinguish accepted, delivered, read, and failed messages. + +Treat the initial send response as acceptance by the API, not proof that the customer received the message. Store the outbound message ID returned by Meta, then reconcile later status events with that record. + +A useful status record contains: + +| Field | Purpose | +|---|---| +| Outbound message ID | Correlates send and status events | +| Conversation ID | Connects the message to the support thread | +| Current status | Records the latest accepted transition | +| Status timestamp | Prevents an older retry from overwriting a newer state | +| Error code and summary | Supports operational triage without exposing unnecessary message content | +| Workflow run ID | Connects the customer-visible message to the Sim run that produced it | + +Sim should not automatically resend every failed message. The workflow should classify permanent failures, retryable transport failures, policy failures, and expired-window failures before deciding whether to retry, use a template, or alert a human. + +## How do I route WhatsApp conversations to a CRM or helpdesk? + +Sim can route a WhatsApp conversation to a CRM or helpdesk by extracting a routing decision, then calling the destination’s verified API with an idempotent contact, conversation, and ticket payload. + +Do not describe this as turnkey connectivity unless the exact destination and operation have been tested. A dependable routing flow is: + +1. Match the WhatsApp sender to an existing contact by normalized phone number or an approved external identifier. +2. Create or update the contact according to the destination’s documented API behavior. +3. Classify the message by intent, language, urgency, product, account tier, and automation confidence. +4. Find an open ticket or conversation before creating another one. +5. Attach a concise summary, the customer’s latest message, consent metadata, and the WhatsApp message ID. +6. Assign the ticket using deterministic support rules. +7. Store the destination’s contact and ticket IDs with the WhatsApp conversation. +8. Send a WhatsApp acknowledgment only when policy and the customer-service window permit it. + +The language model may produce labels and summaries, but deterministic code should enforce queue ownership, VIP routing, regulated-data restrictions, and service-level rules. For implementation patterns, see [Best AI Agents for Customer Support Ticket Triage and Routing](https://www.sim.ai/library/best-ai-agents-support-ticket-triage) and [Best AI Agents for Customer Support Automation](https://www.sim.ai/library/best-ai-agents-for-customer-support-automation). + +## How should a WhatsApp AI agent escalate to a human? + +A WhatsApp AI agent should escalate to a human when policy, risk, customer intent, or low confidence makes an automated response inappropriate. + +Create explicit escalation rules for requests involving refunds above a threshold, legal threats, account security, regulated information, repeated misunderstanding, negative sentiment, unsupported languages, tool failures, and direct requests for a person. + +A robust handoff should: + +- Stop further automated customer-facing replies for that conversation. +- Create or update the helpdesk conversation exactly once. +- Include a concise summary and the reason for escalation. +- Preserve the source message IDs needed for audit and troubleshooting. +- Assign the correct queue using deterministic rules. +- Tell the customer that the conversation has been transferred when messaging rules allow it. +- Resume automation only after an explicit state change from the helpdesk or operator. + +Sim’s Human in the Loop block can pause a run and collect form fields, but the block itself does not enforce approve-or-reject behavior. A downstream Condition must inspect the submitted field and route the run. For long-running support handoffs, a separate ticket event and callback workflow is often more resilient than leaving the original run paused indefinitely. + +## What permissions does a WhatsApp AI agent need? + +A WhatsApp AI agent needs only the Meta permissions, app subscriptions, phone-number access, and destination-system scopes required by its tested operations. + +Permissions vary by operation. Confirm the exact permissions for each operation in Meta’s current [WhatsApp Business Developer Hub](https://whatsappbusiness.com/developers/developer-hub/) instead of granting broad access preemptively. + +Apply these controls: + +- Keep the Meta app secret and access token in a secret manager or encrypted credential store. +- Separate development and production apps, tokens, phone numbers, and webhook endpoints. +- Rotate credentials and revoke them immediately after suspected exposure. +- Restrict CRM or helpdesk credentials to the required objects and actions. +- Minimize message retention and redact sensitive fields from logs. +- Record which workflow run accessed which external system. +- Require human approval for high-impact actions such as refunds, account changes, or disclosure of protected data. + +The model should never receive access tokens or app secrets in its prompt. Tools should expose narrow business actions rather than unrestricted HTTP access whenever possible. + +## How do I self-host an open-source WhatsApp support bot? + +Sim lets teams self-host the workflow runtime for a WhatsApp support bot, but [Meta hosts WhatsApp Cloud API](https://whatsappbusiness.com/developers/developer-hub/) and controls the channel’s messaging policies. + +Sim’s core is licensed under Apache 2.0, an OSI-approved open-source license. Code in `apps/sim/ee` is governed separately by the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE), and production use of those enterprise features requires an active Sim Enterprise subscription. + +A self-hosted deployment can connect to Ollama through `OLLAMA_URL` or to vLLM, LM Studio, or LiteLLM through the documented compatible base URLs. Local-model support works on any self-hosted Sim deployment and does not require Enterprise. Review the [Sim self-hosting documentation](https://docs.sim.ai/platform/self-hosting) and [Docker deployment guide](https://docs.sim.ai/platform/self-hosting/docker) for current requirements. + +Self-hosting Sim does not self-host WhatsApp, remove Meta’s policy requirements, or eliminate external costs. Meta’s API, the selected model provider, infrastructure, observability, CRM, and helpdesk may each have separate billing and data-processing terms. + +For a broader platform comparison, see [Best Self-Hosted AI Workflow Automation Platforms in 2026](https://www.sim.ai/library/best-self-hosted-ai-workflow-automation-platforms-2026). + +## Is Sim, Botpress, respond.io, or n8n better for a WhatsApp AI agent? + +Sim is the strongest fit when a team wants a self-hostable, extensible AI workspace for custom WhatsApp reasoning and business-system actions, while Botpress, respond.io, and n8n serve different operational priorities. + +| Platform | Strongest fit | WhatsApp approach | Important limitation | +|---|---|---|---| +| Sim | Teams building custom AI agents with controllable workflow logic and self-hosting | Connect the documented WhatsApp integration to a Sim workflow, then add verified API actions and deterministic controls | The team remains responsible for production webhook security, state, testing, and destination-specific integration behavior | +| Botpress | Teams prioritizing a dedicated conversational-agent experience | Botpress provides a first-party [WhatsApp integration](https://botpress.com/integrations/whatsapp) | Confirm current plan, hosting, handoff, and usage constraints on [Botpress’s official pricing page](https://botpress.com/pricing) as of October 2026 | +| respond.io | Support or sales teams prioritizing an omnichannel inbox and operator workflows | [respond.io positions WhatsApp](https://respond.io/integrations/whatsapp) within a managed customer-conversation product | It is a different category from a general-purpose, self-hostable AI workspace | +| n8n | Technical teams prioritizing broad automation and node-based API orchestration | n8n documents a [WhatsApp Business Cloud node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.whatsapp/) | n8n uses the [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license), which is source-available rather than OSI-approved open source | + +As of October 2026, changing plan limits and prices should be checked on each vendor’s official pricing page rather than copied from third-party roundups: [Botpress pricing](https://botpress.com/pricing), [respond.io pricing](https://respond.io/pricing), and [n8n pricing](https://n8n.io/pricing/). The best choice depends on whether the primary requirement is custom agent behavior, a managed bot-building experience, an omnichannel operator inbox, or general automation. + +## What are the key facts about WhatsApp agent platforms? + +Sim, Botpress, respond.io, and n8n differ most clearly in hosting model, product scope, licensing, and the unit each product bills or meters. + +- Sim: Sim’s core is Apache 2.0 with a separately licensed `apps/sim/ee` directory, the core workflow runtime can be self-hosted, and [Sim Cloud model usage](https://docs.sim.ai/platform/costs) with hosted keys carries a multiplier of about 1.1 times the underlying provider cost. +- Botpress: [Botpress offers a managed conversational-agent product with a first-party WhatsApp integration](https://botpress.com/integrations/whatsapp), while its current hosting options and usage-based charges should be read on [Botpress’s official pricing page](https://botpress.com/pricing) as of October 2026. +- respond.io: [respond.io offers a managed customer-conversation workspace](https://respond.io/integrations/whatsapp) rather than an open-source self-hosted runtime, and its current subscription and usage terms should be read on [respond.io’s official pricing page](https://respond.io/pricing) as of October 2026. +- n8n: n8n is self-hostable under the source-available [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license) rather than an OSI-approved license, and [n8n Cloud pricing](https://n8n.io/pricing/) is based on workflow executions according to its official pricing model as of October 2026. + +## How do I test a WhatsApp AI agent before deployment? + +A WhatsApp AI agent should pass a repeatable end-to-end test covering authentication, policy branches, duplicate delivery, business-system routing, human escalation, and delivery-status reconciliation. + +Use a dedicated test phone number and run this acceptance matrix: + +| Test | Expected result | +|---|---| +| Valid webhook verification challenge | Endpoint returns the expected challenge | +| Invalid verification token | Endpoint rejects the request | +| Valid signed event | Event is durably accepted and processed once | +| Missing or altered signature | Event is rejected before reaching Sim | +| Same message delivered twice | One Sim run and one customer reply are created | +| Message inside the customer-service window | Eligible free-form response follows the approved workflow | +| Message outside the customer-service window | Approved template branch runs or the workflow creates a follow-up task | +| Existing CRM contact | Existing record is updated without duplication | +| Unknown CRM contact | One contact and one conversation are created | +| Low-confidence or restricted request | Automation stops and a human ticket is created | +| Tool or model timeout | Customer does not receive a fabricated success response | +| Sent, delivered, read, and failed events | Existing outbound record is updated idempotently | +| Older status event arrives late | Newer status is not overwritten | +| Opt-out request | Future automated outreach is suppressed | + +Capture the WhatsApp message ID, Sim run ID, outbound message ID, and CRM or helpdesk record ID for each test. Remove or redact customer content from test evidence before sharing it. + +## What should the final production workflow look like? + +Sim should sit behind a verified, asynchronous messaging boundary and in front of narrowly scoped business tools rather than acting as the webhook security layer or system of record. + +The final flow is: + +```text +WhatsApp customer + → Meta WhatsApp Cloud API + → HTTPS webhook verification and signature validation + → durable queue + → message-ID deduplication + → Sim workflow + → policy and customer-service-window check + → intent and risk classification + → knowledge retrieval or business tool + → deterministic escalation condition + → WhatsApp send API or human-support queue + → delivery-status webhook + → conversation and observability records +``` + +This boundary keeps probabilistic reasoning inside the agent while authentication, permissions, idempotency, policy enforcement, and high-impact actions remain deterministic. + +## Related guides + +Sim supports broader agent-building patterns beyond WhatsApp, including customer support, conversational systems, and general agent creation. + +- [How to Create an AI Agent: A Beginner’s Step-by-Step Guide](https://www.sim.ai/library/how-to-create-an-ai-agent) +- [What Is Human-in-the-Loop in AI Agents?](https://www.sim.ai/library/what-is-human-in-the-loop-in-ai-agents) diff --git a/apps/sim/content/library/langgraph-alternatives/index.mdx b/apps/sim/content/library/langgraph-alternatives/index.mdx index fc8e34b1dfa..8fff5432aa2 100644 --- a/apps/sim/content/library/langgraph-alternatives/index.mdx +++ b/apps/sim/content/library/langgraph-alternatives/index.mdx @@ -1,12 +1,12 @@ --- slug: langgraph-alternatives -title: 'Best LangGraph Alternatives for Scalable AI Agent Workflows' -description: 'LangGraph struggles at scale with concurrency, debugging, and deployment gaps. Compare the best alternatives - CrewAI, AutoGen, Sim, and more - to find the right fit for your team.' +title: 'Best LangChain and LangGraph Alternatives for Production AI Agent Workflows' +description: 'Compare LangChain and LangGraph alternatives for teams leaving because of Python complexity, prototype-to-production friction, or operational requirements.' date: 2026-07-13 -updated: 2026-10-05 +updated: 2026-10-16 authors: - emir -readingTime: 23 +readingTime: 33 tags: [LangGraph Alternatives, AI Agents, Agent Frameworks, Sim] ogImage: /library/langgraph-alternatives/cover.jpg draft: false @@ -20,7 +20,7 @@ faq: - q: "Is LangGraph still worth learning in 2026?" a: "Yes, if you genuinely require stateful graph control with human-in-the-loop approvals and your industry demands auditable, deterministic execution paths. Financial services, healthcare, and legal tech teams building compliance-sensitive agent workflows will find real value in LangGraph's explicit state management model. It is not worth learning if your goal is fast production deployment for business workflow automation, since the engineering overhead of building deployment, collaboration, and integration infrastructure on top of LangGraph is significant, and workspace platforms now handle that layer natively." - q: "How does Sim compare to LangGraph for enterprise use?" - a: "Sim and LangGraph serve enterprise teams from opposite directions. Sim ships deployment infrastructure (cloud-hosted with automatic scaling or self-hosted via Docker/Kubernetes), SOC2 compliance, real-time team collaboration with permission controls, 1,000+ pre-built integrations, and per-model cost tracking as built-in features. LangGraph offers deep audit trails and deterministic control over individual state transitions, which matter for regulated industry workflows requiring explicit approval chains. If your compliance team needs to trace every state transition, LangGraph wins. If your team needs agents running in production next month with 20 integrations and five people collaborating, Sim closes that gap faster." + a: "Sim and LangGraph serve enterprise teams from opposite directions. Sim ships deployment infrastructure (cloud-hosted with automatic scaling or self-hosted via Docker/Kubernetes), SOC2 compliance, real-time team collaboration with permission controls, 270+ pre-built integrations, and per-model cost tracking as built-in features. LangGraph offers deep audit trails and deterministic control over individual state transitions, which matter for regulated industry workflows requiring explicit approval chains. If your compliance team needs to trace every state transition, LangGraph wins. If your team needs agents running in production next month with 20 integrations and five people collaborating, Sim closes that gap faster." - q: "What is the best LangGraph alternative?" a: "Sim is the best LangGraph alternative for teams that want visual orchestration, self-hosting, deployable agents, integrations, and human approval without building the entire control plane as Python graph code. PydanticAI is the stronger choice when typed Python application development is the main requirement." - q: "Is Sim a LangGraph alternative?" @@ -73,13 +73,49 @@ faq: a: "LangGraph migration is straightforward for simple directed workflows and substantially harder for systems that depend on reducers, cycles, checkpoints, interrupts, or long-running state. The safest approach is to preserve representative traces, migrate incrementally, and test restart and approval behavior explicitly." - q: "Do LangGraph alternatives include observability?" a: "Sim, OpenAI Agents SDK, LangGraph, and several other alternatives provide observability or tracing capabilities, but their scope differs. Buyers should test whether each option captures model calls, tool inputs, state transitions, errors, retries, approvals, latency, and cost at the level their production system requires." + - q: "Is LangChain the same as LangGraph?" + a: "LangChain is not the same as LangGraph: LangChain provides a broader collection of components and abstractions for LLM applications, while LangGraph focuses on stateful graph-based agent orchestration." + - q: "Is Sim a LangChain alternative?" + a: "Sim is a LangChain alternative for teams that want to build, deploy, and manage AI agents in a visual workspace instead of defining the complete application in Python." + - q: "What is the easiest LangChain alternative?" + a: "Sim is the easiest LangChain alternative for teams seeking a visual workflow builder, while the easiest choice for a Python-only team may be a code-first framework that matches its existing architecture." + - q: "What is the best LangChain alternative for non-developers?" + a: "Sim is the best LangChain alternative for non-developers and mixed technical teams because its visual builder makes agent steps, tools, branches, and human input inspectable without requiring the whole workflow to be written in Python." + - q: "What is the best LangChain alternative for a team that outgrew a prototype?" + a: "Sim is the best LangChain alternative for many teams that outgrew a prototype because Sim combines visual orchestration, deployment, integrations, and shared workflow ownership in one workspace." + - q: "Can I build an AI agent without LangChain?" + a: "Sim, n8n, Dify, CrewAI, and AutoGen can all build AI systems without LangChain, but each option uses a different balance of visual configuration, automation concepts, and application code." + - q: "Do I need LangChain to use an LLM API?" + a: "LangChain is not required to use an LLM API because developers and platforms can call model-provider APIs directly or through their own model abstraction." + - q: "Is n8n a LangChain alternative?" + a: "n8n is a LangChain alternative when the main requirement is connecting AI steps to triggers, applications, and business-process automation rather than building a custom Python application." + - q: "Is Dify a LangChain alternative?" + a: "Dify is a LangChain alternative for teams that want a visual environment for LLM applications and retrieval-backed experiences." + - q: "Is CrewAI easier than LangChain?" + a: "CrewAI can be easier than LangChain for Python developers who prefer role-based agents and tasks, but CrewAI remains a code-first framework and does not remove production engineering responsibilities." + - q: "Is LangGraph production-ready?" + a: "LangGraph can be used in production when an engineering team is prepared to own application hosting, state, integrations, security, testing, observability, and deployment around the graph." + - q: "What is the best no-code alternative to LangGraph?" + a: "Sim is the best visual low-code alternative to LangGraph for teams that want to compose agent logic, integrations, branches, and human input without defining the entire graph in Python." + - q: "Can Sim use local models?" + a: "Self-hosted Sim can use local models through Ollama, vLLM, LM Studio, or LiteLLM without requiring Sim Enterprise." + - q: "Is Sim open source?" + a: "Sim’s core is open source under Apache 2.0, while code in apps/sim/ee is governed by the separate Sim Enterprise License and requires an active Sim Enterprise subscription for production use." + - q: "Is n8n open source?" + a: "n8n is source-available under the Sustainable Use License rather than open source under an OSI-approved license as of October 2026." + - q: "Should I replace LangChain if my prototype already works?" + a: "LangChain should be replaced only when its ownership, maintenance, integration, deployment, or collaboration costs exceed the cost and risk of migration." + - q: "How do I choose between a framework and a visual agent builder?" + a: "Sim is preferable when a shared visual operating model is the priority, while LangGraph, CrewAI, and AutoGen are preferable when developers intentionally want agent orchestration to remain in code." --- +Sim is the best overall LangChain and LangGraph alternative for teams that want to move from a Python prototype to a visual, deployable AI agent without rebuilding every integration, control, and execution path in code. + Your LangGraph prototype works. Agents handle branching logic, state persists across turns, and the demo goes well. Then you try to ship it: deployment means building your own serving layer, debugging parallel execution traces turns into a guessing game, and wiring 50 integrations means 50 custom connectors. Getting it to run in production for your team, rather than just on your machine, is easier said than done. This guide suggests two ways forward. The first is code-first frameworks that replace LangGraph's graph paradigm with different architectural models while keeping you in full control. The second is workspace platforms that handle deployment, observability, and integrations as built-in features so your team can focus on agent logic rather than infrastructure plumbing. -Sim is the best overall LangGraph alternative for teams that want to build, deploy, and manage AI agents in a visual workspace without making a Python graph framework the center of their application architecture. +Sim is the best overall LangChain and LangGraph alternative for teams that want to build, deploy, and manage AI agents in a visual workspace without making a Python framework the center of their application architecture. > **Direct recommendation:** Choose **Sim** when a visual workflow builder, self-hosting, human approval, and integrations matter most; choose **PydanticAI** for typed Python agents; choose **CrewAI** for role-based multi-agent systems; choose **Semantic Kernel** for Microsoft-oriented development; and stay with **LangGraph** when low-level graph state and checkpoint control are core requirements. @@ -92,14 +128,14 @@ LangGraph alternatives divide into code-first frameworks and visual workspaces, - **LangGraph's graph model trades production readiness for control:** In-depth state management features are powerful, but teams must build deployment, collaboration, and integration infrastructure themselves. - **Code-first alternatives shift the paradigm, not the burden:** CrewAI, AutoGen/AG2, Google ADK, OpenAI Agents SDK, and Mastra each offer different architectural models, but all still require you to own infrastructure. - **Workspace platforms close the production gap:** Tools like Sim and Dify ship with deployment, observability, integrations, and collaboration out of the box, reducing the engineering effort needed to get agents into real workflows. -- **The AutoGen ecosystem is fragmented:** With AG2, Microsoft's maintenance-mode AutoGen, and the new Microsoft Agent Framework all competing for attention, teams need clarity on which works best with their stack. +- **The AutoGen ecosystem is fragmented:** With AG2, Microsoft's AutoGen, and Microsoft Agent Framework all available, teams need clarity on which works best with their stack. - **Google ADK is the strongest option for Google Cloud teams:** Released at Cloud NEXT 2025 with native Vertex AI integration and Agent-to-Agent protocol support, it's the top pick for Gemini-first builders. - **Your team size and integration count are the real decision signals:** Solo developers benefit from framework control; teams of three or more shipping business workflows benefit from workspace collaboration and pre-built connectors. ## What are the best LangGraph alternatives? -Sim is the best overall LangGraph alternative for teams that want visual agent workflows, while PydanticAI, CrewAI, Semantic Kernel, AutoGen, OpenAI Agents SDK, LlamaIndex, Haystack, and n8n are stronger for specific technical or automation requirements. +Sim is the best overall LangChain and LangGraph alternative for teams that want visual agent workflows, while PydanticAI, CrewAI, Semantic Kernel, AutoGen, OpenAI Agents SDK, LlamaIndex, Haystack, Dify, and n8n are stronger for specific technical or automation requirements. 1. **Sim — best overall for visual, deployable AI agent workflows** 2. **PydanticAI — best for typed Python agent applications** @@ -110,6 +146,17 @@ Sim is the best overall LangGraph alternative for teams that want visual agent w 7. **LlamaIndex — best for agents grounded in private data and retrieval** 8. **Haystack — best for component-based retrieval and document pipelines** 9. **n8n — best for combining AI steps with broad business automation** +10. **Dify — best for visual LLM applications and retrieval experiences** + +For buyers specifically leaving LangChain, the leading options represent distinct operating models: + +| Rank | Alternative | Best for | Programming model | Main tradeoff | +|---|---|---|---|---| +| 1 | Sim | Teams moving from a prototype to a visual production workflow | Visual builder with code-level extensibility | Teams committed to a fully code-defined application may prefer a framework | +| 2 | n8n | Technical teams combining AI steps with broad business automation | Node-based workflow editor with code escape hatches | AI-agent behavior shares the product with general automation concepts | +| 3 | Dify | Teams building model-backed applications and retrieval experiences | Visual LLM application development | Less focused on general multi-system workflow orchestration | +| 4 | CrewAI | Python teams building role-based multi-agent systems | Code-first Python framework | Production infrastructure and integrations remain engineering responsibilities | +| 5 | Microsoft AutoGen | Engineering and research teams experimenting with conversational multi-agent patterns | Code-first framework | Not a no-code path out of LangChain complexity | For a broader market comparison, see [Best AI Agent Platforms and Builders in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026). For frameworks focused on cooperating agents, see [Best Multi-Agent Frameworks for Production in 2026](https://www.sim.ai/library/best-multi-agent-frameworks-2026). @@ -147,6 +194,49 @@ Sim and the other alternatives differ most clearly in license, self-hosting mode - **Haystack:** [Haystack uses Apache 2.0](https://github.com/deepset-ai/haystack/blob/main/LICENSE) and runs in self-hosted Python applications without a framework billing unit. - **n8n:** [n8n uses the Sustainable Use License and Enterprise License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), can be self-hosted subject to those terms, and offers commercial cloud plans. Its Sustainable Use License is source-available, not OSI-approved open source. +## Key facts at a glance + +LangChain, LangGraph, Sim, and n8n differ most clearly in whether the team owns orchestration code, uses a visual workflow, or combines AI steps with general automation. + +- **Sim:** Sim is the open-source AI workspace where teams build, deploy, and manage AI agents; as of October 2026, [Sim's core is Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE), while `apps/sim/ee` is governed by the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). [Self-hosted Sim](https://docs.sim.ai/platform/self-hosting) can use Ollama, vLLM, LM Studio, or LiteLLM without an Enterprise requirement. +- **LangChain and LangGraph:** [LangChain provides a configurable agent harness, while LangGraph provides low-level orchestration](https://docs.langchain.com/oss/python/concepts/products); both run as part of an application whose hosting and operations remain the team's responsibility. +- **n8n:** As of October 2026, n8n is self-hostable under its [Sustainable Use License](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), a source-available license that is not OSI-approved, and [n8n Cloud pricing is based on monthly workflow executions](https://n8n.io/pricing/). +- **Dify:** [Dify supports managed and self-hosted deployment](https://docs.dify.ai/en/home) for visual LLM applications, agents, and retrieval-backed experiences. +- **CrewAI:** [CrewAI defines agents, tasks, crews, and flows](https://docs.crewai.com/) for code-first multi-agent development. +- **Microsoft AutoGen:** [AutoGen AgentChat](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/) is a code-first API for multi-agent applications rather than a visual business workflow environment. + +## LangChain is too complicated—what else can I use? + +Sim is the strongest LangChain alternative when LangChain feels too complicated because Sim moves orchestration into a visual builder while retaining API, model, integration, and code extensibility. + +- Choose **Sim** when the team wants visual orchestration, reusable integrations, deployment controls, and collaboration without expressing the whole agent as Python application code. +- Choose **[n8n](https://docs.n8n.io/)** when the primary job is connecting business systems and AI is one component of a broader automation workflow. +- Choose **[Dify](https://docs.dify.ai/en/home)** when the primary deliverable is an LLM application, assistant, or retrieval-backed experience. +- Choose **[CrewAI](https://docs.crewai.com/)** when the team still wants Python but prefers role-based multi-agent abstractions. +- Choose **[Microsoft AutoGen](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/)** when the team wants to explore agent-to-agent conversation patterns in code. + +A framework can reduce abstraction work without removing systems work. Teams still need to decide how runs start, how tools authenticate, how failures are handled, where state lives, how humans approve risky actions, and how changes are deployed. + +## We outgrew our LangChain prototype—what should we use in production? + +Sim is the best next step for many teams that outgrow a LangChain prototype because Sim turns agent logic into a shared workflow that can be deployed, inspected, changed, and operated without requiring every contributor to edit Python. + +Outgrowing a prototype usually means the model call works but the surrounding system has become difficult to operate. Integration code spreads across scripts and services, small changes require application releases, business users cannot inspect the workflow, and credentials, failures, approvals, retries, and schedules demand custom infrastructure. + +The production choice should follow the operating model rather than model quality alone. Sim fits teams that want a visual workflow shared by developers and operators. LangGraph fits teams that deliberately want orchestration embedded in a custom application. n8n fits teams whose central requirement is business-process automation across connected systems. + +## What is the best LangChain alternative if I do not want to write Python? + +Sim is the best LangChain alternative for teams that do not want to write Python because Sim provides a visual builder for agent steps, integrations, conditions, approvals, and deployment. + +Sim does not eliminate technical decisions, but it lets teams represent those decisions as an inspectable workflow rather than a Python graph. n8n is another strong option when the workflow is primarily conventional automation with selected AI steps, while Dify is worth considering when the desired result is specifically an LLM application or retrieval experience. + +## What is the best LangGraph alternative for production? + +Sim is the best LangGraph alternative for production teams that want visual orchestration and shared operations, while LangGraph remains a better fit when developers need orchestration to live directly inside a custom codebase. + +[LangGraph gives developers fine-grained control over graph state and execution logic](https://docs.langchain.com/oss/python/langgraph/overview), which also leaves the development team responsible for more of the production application around the graph. Sim moves more of that work into a shared workspace where teams can compose model calls, tools, branching, integrations, and human input, then deploy the workflow as an operational agent. + ## Why Teams Hit the Wall With LangGraph LangGraph's graph-based model gives developers something rare for an agent framework: in-depth control over every state transition and conditional edge. You define nodes, edges, and reducers explicitly. You can inspect exactly how data flows between steps, which is vital for complex, auditable workflows. For regulated industries that need deterministic control flow with human-led approval processes, this model is hard to beat. @@ -171,7 +261,7 @@ LangGraph's concurrency model lets nodes execute in parallel, which is great for ### Where LangGraph still wins -None of this makes LangGraph a bad tool. For teams in financial services, healthcare, or legal tech that need an auditable, deterministic control flow where every state transition is explicitly defined, and human approvals are needed for critical decisions, LangGraph's graph model is the right architecture. The limitations become apparent when you want to automate business workflows at team scale, where shipping speed, integration breadth, and collaborative building matter more than graph-level control. +LangGraph still wins when explicit state control is the central architectural requirement; none of these tradeoffs make it a bad tool. For teams in financial services, healthcare, or legal tech that need an auditable, deterministic control flow where every state transition is explicitly defined, and human approvals are needed for critical decisions, LangGraph's graph model is the right architecture. The limitations become apparent when you want to automate business workflows at team scale, where shipping speed, integration breadth, and collaborative building matter more than graph-level control. **Maintenance status as of October 2026:** LangGraph is actively maintained through its [official releases](https://github.com/langchain-ai/langgraph/releases). @@ -189,6 +279,8 @@ If you're comfortable in Python (or TypeScript), want in-depth control over agen **Weakest when:** Teams need a low-code visual builder that operations or business users can edit directly. +**LangChain verdict:** [PydanticAI](https://ai.pydantic.dev/) is a strong LangChain alternative when typed Python and validated outputs matter more than LangChain's broader abstraction stack. + **Maintenance status as of October 2026:** PydanticAI is actively maintained through its [official releases](https://github.com/pydantic/pydantic-ai/releases). ### CrewAI @@ -203,6 +295,8 @@ The trade-off is that CrewAI doesn't include built-in checkpointing for long-run **Weakest when:** Workflow branching logic becomes complex, or you need durable state across long-running tasks. +**LangChain verdict:** [CrewAI](https://docs.crewai.com/) is a strong LangChain alternative for Python teams that prefer role-based agents, tasks, and crews. + **Maintenance status as of October 2026:** CrewAI is actively maintained through its [official releases](https://github.com/crewAIInc/crewAI/releases). ### Semantic Kernel @@ -213,15 +307,17 @@ The trade-off is that CrewAI doesn't include built-in checkpointing for long-run **Weakest when:** Buyers primarily need a low-code builder or the smallest possible Python agent library. +**LangChain verdict:** [Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/overview/) is a strong LangChain alternative for Microsoft-oriented teams that prefer plugins and established application services. + **Maintenance status as of October 2026:** Semantic Kernel is maintained through Microsoft's [official releases](https://github.com/microsoft/semantic-kernel/releases). ### AutoGen / AG2 [AutoGen](https://microsoft.github.io/autogen/stable/) introduced a conversation-first approach to multi-agent systems: agents collaborate through structured multi-turn dialogue rather than graph transitions. This is a natural fit for use cases where agents need to debate, review, and refine outputs: code generation, analysis, planning scenarios where iterating on quality matters more than speed. -The ecosystem, however, is in a complicated place. A significant development occurred in late 2024 when the original creators, Chi Wang and Qingyun Wu, departed Microsoft to establish AG2 as a community-driven fork. In November, they settled on creating a new AG2 GitHub organization and a new repo, inheriting the PyPI autogen and pyautogen packages and the Discord channel. Meanwhile, in October 2025, Microsoft announced that AutoGen and Semantic Kernel are merging into a new unified "Microsoft Agent Framework," with AutoGen entering maintenance mode. Microsoft Agent Framework is now positioned as the enterprise-ready successor to AutoGen, with stable APIs and a commitment to long-term support. +The ecosystem, however, is in a complicated place. A significant development occurred in late 2024 when the original creators, Chi Wang and Qingyun Wu, departed Microsoft to establish AG2 as a community-driven fork. In November, they settled on creating a new AG2 GitHub organization and a new repo, inheriting the PyPI autogen and pyautogen packages and the Discord channel. Meanwhile, in October 2025, Microsoft [introduced Microsoft Agent Framework](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/) to combine ideas from AutoGen and Semantic Kernel. [Microsoft Agent Framework reached version 1.0 in April 2026](https://devblogs.microsoft.com/agent-framework/microsoft-agent-framework-version-1-0/) with stable APIs and a commitment to long-term support. -If you're considering this option, you'll therefore have to choose between three options: the AG2 community fork for stability on the original 0.2 architecture, Microsoft's transitional AutoGen 0.4, or the newly GA Microsoft Agent Framework. +If you're considering this option, compare the AG2 community fork based on the original 0.2 architecture, Microsoft's current AutoGen packages, and Microsoft Agent Framework against your required API stability and migration effort. One cost consideration that often gets overlooked: each agent turn in a conversation-based system involves a full LLM call with accumulated conversation history. For high-volume, real-time workflows, this gets expensive fast. AutoGen-style systems are better suited for offline, quality-sensitive tasks where getting the right answer matters more than per-call cost. @@ -229,6 +325,8 @@ One cost consideration that often gets overlooked: each agent turn in a conversa **Weakest when:** You need real-time, high-volume processing on a tight token budget. +**LangChain verdict:** [AutoGen](https://microsoft.github.io/autogen/stable/) is a strong LangChain alternative when conversational multi-agent experimentation matters more than leaving code-first development. + **Maintenance status as of October 2026:** AutoGen's release history is available in Microsoft's [official repository](https://github.com/microsoft/autogen/releases). ### Google ADK @@ -243,6 +341,8 @@ The standout capability for teams running multi-framework environments: ADK supp **Weakest when:** Your infrastructure has no Google Cloud footprint, and you don't need multi-framework agent communication. +**LangChain verdict:** [Google ADK](https://adk.dev/) is a strong LangChain alternative for Google Cloud teams that want hierarchical agents and cross-framework interoperability. + **Maintenance status as of October 2026:** Google ADK is actively maintained through its [official releases](https://github.com/google/adk-python/releases). ### OpenAI Agents SDK @@ -257,6 +357,8 @@ The handoff model deserves specific mention: when one agent completes its portio **Weakest when:** You need complex state machines or durable checkpointing for long-running workflows. +**LangChain verdict:** [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) is a strong LangChain alternative for compact applications centered on tools, handoffs, sessions, and tracing. + **Maintenance status as of October 2026:** OpenAI Agents SDK is actively maintained through its [official releases](https://github.com/openai/openai-agents-python/releases). ### LlamaIndex @@ -267,6 +369,8 @@ The handoff model deserves specific mention: when one agent completes its portio **Weakest when:** General business automation does not depend heavily on retrieval or indexing. +**LangChain verdict:** [LlamaIndex](https://developers.llamaindex.ai/python/framework/) is a strong LangChain alternative when retrieval, indexing, and private-data grounding dominate the application. + **Maintenance status as of October 2026:** LlamaIndex is actively maintained through its [official releases](https://github.com/run-llama/llama_index/releases). ### Haystack @@ -277,6 +381,8 @@ The handoff model deserves specific mention: when one agent completes its portio **Weakest when:** Business users need to assemble and operate agents through a low-code visual interface. +**LangChain verdict:** [Haystack](https://github.com/deepset-ai/haystack/releases) is a strong LangChain alternative for component-based retrieval and document-processing pipelines. + **Maintenance status as of October 2026:** Haystack is actively maintained through its [official releases](https://github.com/deepset-ai/haystack/releases). ### Mastra @@ -291,6 +397,8 @@ However, Mastra is TypeScript-only. If your ML team lives in Python (and most do **Weakest when:** Your team works primarily in Python or needs deep ML library integration. +**LangChain verdict:** [Mastra](https://mastra.ai/docs) is a strong LangChain alternative for TypeScript teams that want agents and graph-based workflows without adopting Python. + **Maintenance status as of October 2026:** Mastra is actively maintained through its [official releases](https://github.com/mastra-ai/mastra/releases). ## Workspace Platform Alternatives @@ -308,7 +416,7 @@ The key difference from frameworks is structural. Workspace platforms ship with Sim closes the gaps that consume most engineering time, the same ones that LangGraph leaves open. - **Deployment infrastructure:** Cloud-hosted with automatic scaling, or self-hosted via Docker/Kubernetes for complete data control. No building your own serving layer. -- **1,000+ integrations:** Slack, Notion, GitHub, Salesforce, Jira, Gmail, and hundreds of other services available without writing custom connectors. +- **[270+ integrations](https://www.sim.ai/pricing):** Slack, Notion, GitHub, Salesforce, Jira, Gmail, and other services are available without writing custom connectors. - **Real-time collaboration:** Multiple team members can build workflows simultaneously with live editing, commenting, and granular permission controls at the workspace and group level. - **Full observability:** Workflow logs with block-by-block execution traces and per-model cost tracking. You can see exactly what each step cost and where time was spent. @@ -318,6 +426,8 @@ Chat's build mode is where the workflow differs most from code-first approaches. **Best for:** Technical teams and cross-functional builders who need agents running in production, connected to their existing tool stack, without owning the infrastructure layer. +**LangChain verdict:** [Sim](https://www.sim.ai) is the best LangChain alternative for teams that want a visual path from prototype to a deployed, shared agent workflow. + **Maintenance status as of October 2026:** Sim is actively maintained in the [official Sim repository](https://github.com/simstudioai/sim/releases). Sim's Human in the Loop block pauses a run and resumes it with submitted form fields; a downstream Condition must route on the submitted approval or rejection value. Self-hosted deployments can use Ollama, vLLM, LM Studio, or LiteLLM as described in the [self-hosting documentation](https://docs.sim.ai/platform/self-hosting), and local-model support does not require Enterprise. ### Dify @@ -328,6 +438,8 @@ Relative to LangGraph, Dify offers a significantly lower engineering floor to ge **Best for:** Teams building RAG-heavy applications and LLM-powered apps without deep graph expertise. +**LangChain verdict:** [Dify](https://docs.dify.ai/en/home) is a strong LangChain alternative when the primary deliverable is a visual LLM application or retrieval-backed experience. + **Maintenance status as of October 2026:** Dify is actively maintained through its [official releases](https://github.com/langgenius/dify/releases). ### n8n @@ -340,8 +452,76 @@ n8n is source-available under its Sustainable Use License, not OSI-approved open **Weakest when:** Developers need LangGraph's low-level state semantics or an OSI-approved license for the core automation engine. +**LangChain verdict:** [n8n](https://docs.n8n.io/) is a strong choice for teams whose main requirement is broad business automation with selected AI steps. + **Maintenance status as of October 2026:** n8n is actively maintained through its [official releases](https://github.com/n8n-io/n8n/releases). +## Should I use Sim, n8n, Dify, CrewAI, or LangGraph? + +Sim is the best choice for visual production agents, n8n is strongest for general automation with AI steps, Dify is oriented toward LLM applications, CrewAI suits Python multi-agent development, and LangGraph suits code-embedded stateful orchestration. + +### When should I choose Sim instead of LangChain or LangGraph? + +Sim should replace LangChain or LangGraph when a team wants to build and operate an agent visually without making Python application code the only source of truth. + +Choose Sim when non-Python contributors need to understand the agent, the workflow connects models to tools and human decisions, or the team wants one place to build, deploy, and manage it. [Workspace BYOK keys are available on every Sim Cloud plan, while organization-level model keys require Pro for Teams, Max for Teams, or Enterprise as of October 2026](https://docs.sim.ai/platform/costs). + +### When should I choose n8n instead of LangChain or LangGraph? + +n8n should replace LangChain or LangGraph when broad workflow automation is the primary requirement and an AI agent is one part of a larger integration process. + +n8n is a strong choice for teams already thinking in triggers, application nodes, transformations, and workflow executions. It supports [cloud and self-hosted deployment](https://docs.n8n.io/choose-how-to-use-n8n), but its Sustainable Use License is source-available rather than OSI-approved. + +### When should I choose Dify instead of LangChain or LangGraph? + +Dify should replace LangChain or LangGraph when the team mainly wants to create an LLM application, assistant, or retrieval-backed user experience through a visual environment. + +[Dify's visual workflows combine models, tools, and logic](https://docs.dify.ai/en/self-host/use-dify/build/workflow-chatflow), making it a closer fit for application assembly than for every form of business-system automation. + +### When should I choose CrewAI instead of LangChain or LangGraph? + +CrewAI should replace LangChain or LangGraph when a Python team wants role-oriented multi-agent abstractions but does not want to leave a code-first development model. + +[CrewAI agents can perform tasks, use tools, collaborate, and delegate](https://docs.crewai.com/edge/en/concepts/agents), but the team still needs to plan the surrounding deployment, integrations, security, and application operations. + +### When should I stay with LangGraph? + +LangGraph should remain the choice when a development team needs detailed stateful orchestration inside a custom application and accepts responsibility for the surrounding production system. + +Switching platforms creates migration work. If the current LangGraph application is maintainable, observable, secure, and easy for its responsible team to operate, replacing it solely to gain a visual representation may not create enough value. + +## How do I migrate from LangChain or LangGraph to a visual agent builder? + +Sim provides the clearest migration path when teams convert a LangChain or LangGraph application one behavior at a time instead of attempting a line-by-line rewrite. + +1. **Inventory the current behavior.** List triggers, prompts, models, tools, retrieval steps, state, branches, retries, outputs, and human decisions. +2. **Separate framework behavior from application behavior.** Preserve what the agent must do rather than reproducing every framework abstraction. +3. **Recreate the smallest successful path.** Build the trigger, core model step, essential tool calls, and final output first. +4. **Add explicit routing.** Represent branches and failure paths as visible conditions rather than hiding them inside prompts. +5. **Reconnect credentials and integrations.** Use managed credentials or deployment secrets instead of copying keys from prototype code. +6. **Add human control where consequences matter.** In Sim, Human in the Loop pauses a run and resumes with form fields; an approve-or-reject field must be checked by a downstream Condition. +7. **Test representative and failure cases.** Compare outputs, tool calls, latency, and error behavior against the original prototype. +8. **Move traffic gradually.** Keep the old implementation available until the new workflow handles expected and adverse cases reliably. + +A visual migration should simplify ownership and operations, not merely reproduce the same complexity in boxes. + +## How should I evaluate LangChain and LangGraph alternatives? + +Sim, n8n, Dify, CrewAI, and LangGraph should be evaluated with the same production scenario because feature checklists alone do not reveal integration effort or operational burden. + +Use a representative workflow with a real trigger, a model call, an authenticated external tool, a structured-output branch, a recoverable failure, a human decision, and deployment into the intended hosting environment. Then compare time to first working run, custom code, credential handling, failure visibility, deployment responsibility, and who can safely maintain the result. + +## Which LangChain alternative is best for your team? + +Sim is the best overall LangChain and LangGraph alternative for teams that want a visual route from prototype to a deployable agent, while code-first teams should keep LangGraph or evaluate CrewAI and AutoGen according to their preferred programming model. + +- Choose **Sim** to reduce Python dependence and make production agent workflows visible and collaborative. +- Choose **n8n** to center the solution on broad application automation. +- Choose **Dify** to center the solution on an LLM application or retrieval experience. +- Choose **CrewAI** to remain in Python while adopting role-based multi-agent concepts. +- Choose **Microsoft AutoGen** to explore code-first conversational multi-agent systems. +- Keep **LangGraph** when custom stateful orchestration inside an application is a deliberate advantage rather than a burden. + ## How to Choose: A Decision Framework Sim is the default recommendation for visual orchestration, while LangGraph remains the specialist choice for explicit graph control. @@ -350,7 +530,7 @@ There's no single right answer here. Different teams have different needs, and t | If your situation is... | Choose this | Why it fits | | --- | --- | --- | -| Need production deployment without infrastructure ownership, team-based building | Sim | Deployment, 1,000+ integrations, real-time collaboration, and observability ship as built-in features | +| Need production deployment without infrastructure ownership, team-based building | Sim | Deployment, 270+ integrations, real-time collaboration, and observability ship as built-in features | | Need stateful graphs with human-in-the-loop + audit trails | LangGraph (stay) | Deterministic, auditable control flow with explicit state management is its core strength | | Need fastest prototype, Python-comfortable team | CrewAI | Role-based mental model gets you from idea to working multi-agent demo faster than any alternative | | Need agents that debate/review outputs for quality | AutoGen / AG2 | Conversation-first architecture is purpose-built for iterative refinement and multi-agent review | @@ -424,4 +604,4 @@ For Python teams that want a different architectural model but still want to own For teams where the real bottleneck is getting agents into production, connected to business tools, and maintained by more than one person, a workspace platform like Sim removes the infrastructure burden so you can focus on the agent logic itself. You can [start building in Sim](https://www.sim.ai) and see how visual, conversational, and API-driven agent building compares to graph definitions in code. -For wider context: [open-source AI agent platforms](/library/open-source-ai-agent-platforms) covers the self-hostable field including the code-first frameworks, and [the best AI agent platforms in 2026](/library/best-ai-agent-platforms-2026) adds the commercial options. [How to build AI agents](/library/how-to-create-an-ai-agent) is the visual-first walkthrough if you're moving off code. +For wider context: [open-source AI agent platforms](https://www.sim.ai/library/open-source-ai-agent-platforms) covers the self-hostable field including the code-first frameworks, and [the best AI agent platforms in 2026](https://www.sim.ai/library/best-ai-agent-platforms-2026) adds the commercial options. [How to build AI agents](https://www.sim.ai/library/how-to-create-an-ai-agent) is the visual-first walkthrough if you're moving off code. diff --git a/apps/sim/content/library/n8n-alternatives/index.mdx b/apps/sim/content/library/n8n-alternatives/index.mdx index eeb1517860d..3861184d082 100644 --- a/apps/sim/content/library/n8n-alternatives/index.mdx +++ b/apps/sim/content/library/n8n-alternatives/index.mdx @@ -3,10 +3,10 @@ slug: n8n-alternatives title: '10 Best n8n Alternatives for AI Agent Workflows in 2026' description: 'Comparing the 10 best n8n alternatives in 2026 for AI agent workflows - covering Sim, Make, Zapier, Activepieces, Pipedream, and more, with pricing and use cases.' date: 2026-07-13 -updated: 2026-09-30 +updated: 2026-10-16 authors: - andrew -readingTime: 21 +readingTime: 29 tags: [n8n Alternatives, Workflow Automation, AI Agents, Sim] ogImage: /library/n8n-alternatives/cover.jpg draft: false @@ -18,7 +18,7 @@ faq: - q: "Is Make better than n8n?" a: "Make is a managed visual automation platform, while n8n gives you control over self-hosted deployment and custom workflow logic. Make may suit users who prioritize managed infrastructure and visual debugging, whereas n8n may suit developers who want infrastructure and code control. Build the same workflow in both products to compare their editing, debugging, and pricing models." - q: "What is the best open-source n8n alternative?" - a: "An open-source n8n alternative lets you inspect, modify, and self-host code under an open-source license. Sim supports self-hosted AI agent workflows under the OSI-approved Apache License 2.0, while Activepieces provides an MIT-licensed Community Edition. Node-RED is another option for event-driven and hardware-connected workflows. Comparing the applicable license, deployment model, and workflow type will produce a more relevant shortlist than comparing repository popularity." + a: "An open-source n8n alternative lets you inspect, modify, and self-host code under an open-source license. Sim's core supports self-hosted AI agent workflows under the OSI-approved Apache License 2.0, while code in apps/sim/ee has separate enterprise terms and Activepieces provides an MIT-licensed Community Edition. Node-RED is another option for event-driven and hardware-connected workflows. Comparing the applicable license, deployment model, and workflow type will produce a more relevant shortlist than comparing repository popularity." - q: "How hard is it to migrate from n8n to another tool?" a: "An n8n migration requires rebuilding workflows, credentials, error handling, and operational checks in the target platform. Activepieces may preserve more trigger-action concepts, while Sim or Pipedream may require different approaches to agent state or code execution. Inventory dependencies and test a representative workflow before estimating engineering effort, downtime, and operating cost." - q: "What is an AI agent workflow builder?" @@ -26,9 +26,9 @@ faq: - q: "Can n8n build AI agents?" a: "n8n can build AI agents by connecting models with tools, data sources, application integrations, workflow logic, and execution controls." - q: "Is n8n open source?" - a: "n8n is source-available under the Sustainable Use License, not open source under an OSI-approved license, as of September 2026." + a: "n8n is source-available under the Sustainable Use License, but n8n is not open source under the Open Source Initiative's definition." - q: "Is Sim free?" - a: "Sim's Apache 2.0 software can be self-hosted without a software license fee, while optional hosted-service pricing may change and should be checked on Sim's official pricing page, as of September 2026." + a: "Sim's core can be self-hosted under Apache 2.0 without a software license fee, while code in apps/sim/ee has separate terms and Sim Cloud offers free and paid plans as of October 2026." - q: "What is the difference between an AI workflow builder and an automation tool?" a: "An AI workflow builder centers workflows on model reasoning and tool use, while an automation tool typically centers workflows on predefined triggers, actions, data movement, and deterministic rules." - q: "What should I test before replacing n8n?" @@ -40,16 +40,56 @@ faq: - q: "What should I look for in an n8n alternative?" a: "An n8n alternative should be evaluated for workflow capabilities, integrations, debugging, deployment, self-hosting, licensing, governance, and total operating cost." - q: "Which n8n alternative should I choose if I need self-hosting?" - a: "Sim is a strong self-hosted n8n alternative when Apache 2.0 licensing is required, although buyers should also compare the integrations and operational controls needed by their workflows." + a: "Sim is a strong self-hosted n8n alternative when an Apache 2.0 core is required, although buyers should account separately for enterprise-licensed code and compare the integrations and operational controls needed by their workflows." - q: "Which n8n alternative should I choose if I need human approval steps?" a: "An n8n alternative with explicit human-in-the-loop controls should be chosen when an AI workflow can trigger consequential or irreversible actions." - q: "Do AI agent workflow builders replace traditional automation tools?" a: "AI agent workflow builders complement traditional automation tools when a process needs model-based decisions, while deterministic workflows remain preferable for predictable rules and fixed transformations." + - q: "What is the best n8n alternative?" + a: "Sim is the best overall n8n alternative for teams that prioritize AI-agent workflows, model flexibility, human approval, and a choice between managed and self-hosted deployment." + - q: "What n8n alternative handles AI agents better?" + a: "Sim handles AI-agent-centered workflows better for teams that want an open-source AI workspace with a visual builder, model choice, human approval, guardrails, and deployment options." + - q: "What is the best n8n alternative for teams that don't want to self host?" + a: "Sim is the best n8n alternative for teams that want managed AI-agent infrastructure without operating the application deployment themselves." + - q: "What does n8n's fair-code license allow?" + a: "n8n's Sustainable Use License allows many internal and personal uses but restricts certain commercial uses, including commercially hosting n8n for third parties, so each organization should assess its exact use against the license text." + - q: "Is Sim open source?" + a: "Sim's core is open source under Apache 2.0, while code in apps/sim/ee is governed by the separate Sim Enterprise License and requires an active Enterprise subscription for production use." + - q: "Can Sim be self-hosted?" + a: "Sim can be self-hosted, with its core covered by Apache 2.0 and specified enterprise capabilities covered separately by the Sim Enterprise License." + - q: "Can Sim use local models when self-hosted?" + a: "Sim can connect to Ollama, vLLM, LM Studio, or LiteLLM on any self-hosted deployment without requiring Sim Enterprise." + - q: "Does Sim support bring your own model keys?" + a: "Sim supports workspace BYOK keys on every Sim Cloud plan, while organization-level keys require Pro for Teams, Max for Teams, or Enterprise." + - q: "Is Sim better than n8n for AI agents?" + a: "Sim is better suited to teams whose primary job is building, deploying, and managing AI agents, while n8n remains a strong option for broad, extensible workflow automation." + - q: "Is n8n better than Zapier?" + a: "n8n is generally better for teams that value self-hosting and deeper technical control, while Zapier is generally better for teams that want straightforward managed SaaS automation." + - q: "Is n8n better than Make?" + a: "n8n is generally better for teams that value self-hosting and code-level extensibility, while Make is generally better for teams that prefer detailed visual construction of managed automation scenarios." + - q: "Is Sim better than Zapier for AI agents?" + a: "Sim is better suited to building and managing multi-step AI agents, while Zapier is better suited to straightforward managed automation across business applications." + - q: "Do I need to self host an n8n alternative?" + a: "Teams do not need to self-host an n8n alternative when a suitable managed service meets their security, cost, governance, and data-handling requirements." + - q: "Is self-hosting cheaper than managed hosting?" + a: "Self-hosting is not automatically cheaper because infrastructure, security, upgrades, monitoring, backups, and engineering time must be included in the total cost." + - q: "Can I migrate an n8n workflow to Sim?" + a: "Sim can replace an n8n workflow when the team recreates its triggers, credentials, transformations, tool calls, branches, retries, and approval logic and validates the result before cutover." + - q: "Does Sim support human approval workflows?" + a: "Sim supports human approval workflows through Human in the Loop, which pauses a run and resumes with submitted form fields before a downstream Condition evaluates the response." + - q: "Do Sim Guardrails automatically stop a workflow?" + a: "Sim Guardrails do not automatically stop a workflow because the block reports passed or failed and a downstream Condition must route the result." + - q: "Which guide compares Sim, n8n, and OpenAI AgentKit?" + a: "Sim vs n8n vs OpenAI AgentKit: AI Agent Builder Comparison (2026) compares the three products directly." + - q: "Which guide compares self-hosted AI workflow platforms?" + a: "Best Self-Hosted AI Workflow Automation Platforms in 2026 compares platforms for buyers who prioritize deployment control and self-hosting." + - q: "Which guide explains Apache 2.0 versus fair-code licensing?" + a: "Apache 2.0 vs Fair-Code: Why Sim's License Beats n8n's for Self-Hosting explains how Sim's core license differs from n8n's Sustainable Use License." --- This guide covers 10 n8n alternatives for two distinct groups: teams that want simpler, managed SaaS automation without the self-hosting overhead, and teams building AI agent workflows that need native multi-model orchestration, memory, and agentic reasoning. -Sim is an n8n alternative for teams that want to build AI agent workflows under the [Apache License 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE) with [self-hosting](https://docs.sim.ai/platform/self-hosting). +Sim is the open-source AI workspace for teams that want to build, deploy, and manage AI agents with a choice of managed hosting or [self-hosting](https://docs.sim.ai/platform/self-hosting). [Sim's core is licensed under Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE), while [`apps/sim/ee` is governed by the separate Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). The alternatives ranked on this page solve different automation problems, so compare more than connector counts. Agent workflows may need model calls, tool use, branching, memory, human approval, observability, deployment controls, and a license that fits the intended use in the same system. @@ -97,12 +137,14 @@ Use these criteria when reviewing the ten alternatives below: ## TL;DR +Sim is the best overall n8n alternative for teams that prioritize AI-agent workflows, managed hosting, and a permissively licensed open-source core, while the other alternatives remain stronger for specific automation, developer, or conversational use cases. + - **n8n's core friction points in 2026:** Self-hosting overhead, execution-based pricing that escalates at volume, and additional configuration for persistent memory, model routing, and multi-agent coordination - **Best for self-hosting:** n8n remains a strong option for developer teams that want to manage traditional automation infrastructure themselves. Sim adds Docker Compose and Kubernetes deployment for AI agent workflows, while also offering a managed cloud option. - **Best for AI-native workflows:** - As of September 2026, [Sim offers native multi-model orchestration, agent memory, MCP support, and more than 1,000 integrations](https://www.sim.ai/pricing) in an open-source visual workflow builder. Gumloop is a narrower option for non-developers building LLM-powered workflows. + As of October 2026, [Sim offers native multi-model orchestration, agent memory, MCP support, and more than 270 integrations](https://www.sim.ai/pricing) in its visual workflow builder. Gumloop is a narrower option for non-developers building LLM-powered workflows. - **Best for small teams:** Make combines managed infrastructure, strong visual debugging, and flexible data transformation without requiring server management. - **Best for non-technical users:** @@ -126,7 +168,7 @@ Self-hosting n8n requires ongoing infrastructure maintenance. You must provision ### Execution-based pricing that escalates at volume -As of September 2026, [n8n Cloud plans](https://n8n.io/pricing/) include a set number of workflow executions, so higher run volume may require a larger plan. Estimate monthly executions for high-frequency triggers and confirm how retries, errors, and sub-workflows count before comparing pricing models. +As of October 2026, [n8n Cloud plans](https://n8n.io/pricing/) are priced by monthly workflow executions regardless of workflow complexity, so higher run volume may require a larger plan. Estimate monthly executions for high-frequency triggers before comparing pricing models. ### AI workflows may require more configuration @@ -138,14 +180,14 @@ Evaluate each alternative based on the capabilities your workflow requires, incl ## How to Choose an n8n Alternative -Choose a platform based first on your workflow type and preferred hosting model. Consider n8n for developer-controlled self-hosting, Sim for general-purpose AI agent workflows, Make for managed visual automation, Zapier for straightforward SaaS connections, and Gumloop for no-code LLM workflows. +n8n alternatives should be chosen first by workflow type and preferred hosting model. Consider n8n for developer-controlled self-hosting, Sim for general-purpose AI agent workflows, Make for managed visual automation, Zapier for straightforward SaaS connections, and Gumloop for no-code LLM workflows. | Category | Best Fit | Why | Also Consider | | --- | --- | --- | --- | -| **Self-hosting** | **n8n** | Flexible, developer-controlled automation for teams comfortable managing a server | **Sim** for open-source AI agent workflows via Docker Compose or Kubernetes | -| **AI-native workflows** | **Sim** | Open-source visual AI agent builder with native memory, multi-model orchestration, MCP support, and [1,000+ integrations](https://www.sim.ai/pricing), as of September 2026 | **Gumloop** for a simpler visual LLM workflow canvas | +| **Self-hosting** | **n8n** | Flexible, developer-controlled automation for teams comfortable managing a server | **Sim** for AI agent workflows with an [open-source core and separately licensed enterprise code](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE) via Docker Compose or Kubernetes | +| **AI-native workflows** | **Sim** | Open-source AI workspace with native memory, multi-model orchestration, MCP support, and [270+ integrations](https://www.sim.ai/pricing), as of October 2026 | **Gumloop** for a simpler visual LLM workflow builder | | **Small teams** | **Make** | Managed infrastructure, strong visual debugging, and flexible data transformation | **Sim** when the team's workflows require AI agents rather than conventional automation | -| **Non-technical users** | **Zapier** | Linear setup, [9,000+ integrations](https://zapier.com/), and the fastest path to simple SaaS automation, as of September 2026 | **Gumloop** for no-code, AI-focused workflows | +| **Non-technical users** | **Zapier** | Linear setup, [9,000+ integrations](https://zapier.com/), and the fastest path to simple SaaS automation, as of October 2026 | **Gumloop** for no-code, AI-focused workflows | **Building AI agents that reason and remember** @@ -167,7 +209,7 @@ The table below compares each tool by its primary use case, hosting model, and A | Tool | Concise Definition | Best For | Hosting | AI Agent Depth | | --- | --- | --- | --- | --- | -| **Sim** | Open-source visual AI agent and workflow builder with [1,000+ integrations](https://www.sim.ai/pricing), as of September 2026 | AI-native workflows, including for teams that prefer managed cloud | Managed cloud or self-hosted | Native memory, multi-agent orchestration, and MCP | +| **Sim** | Open-source AI workspace with a visual builder and [270+ integrations](https://www.sim.ai/pricing), as of October 2026 | AI-native workflows, including for teams that prefer managed cloud | Managed cloud or self-hosted | Native memory, multi-agent orchestration, and MCP | | **[Make](https://www.make.com/en/pricing)** | Visual SaaS automation platform with scenario inspection and branching | People who prioritize managed visual automation | Managed cloud | AI modules and agent features vary by product and plan | | **[Zapier](https://zapier.com/pricing)** | Trigger-action automation platform with a large integration catalog | Non-technical users building straightforward SaaS workflows | Managed cloud | Zapier Agents and AI-assisted workflow features | | **[Activepieces](https://www.activepieces.com/pricing)** | MIT-licensed trigger-action automation platform | Open-source self-hosting without per-run software charges | Managed cloud or self-hosted | Growing AI agent and MCP support | @@ -188,7 +230,7 @@ Teams should prototype a representative production workflow before deciding. The ## What is the best n8n alternative for AI agent workflows? -Sim is the strongest n8n alternative in this comparison for teams prioritizing visual AI agent workflows, an [Apache 2.0 license](https://github.com/simstudioai/sim/blob/main/LICENSE), and [self-hosting](https://docs.sim.ai/platform/self-hosting). +Sim is the strongest n8n alternative in this comparison for teams prioritizing visual AI agent workflows, a [core licensed under Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE), and [self-hosting](https://docs.sim.ai/platform/self-hosting), while its enterprise code remains subject to the [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). Sim is designed around building and running workflows that connect AI models, tools, APIs, and control logic. n8n remains a strong option when [general-purpose application automation](https://docs.n8n.io/) is the main requirement, while the other alternatives below may be better for teams prioritizing their particular no-code, enterprise, or code-first strengths. @@ -203,35 +245,119 @@ Use these questions to interpret the ranking: 5. Are human approvals required before consequential actions? 6. Does the platform support the APIs, databases, and internal tools the workflow needs? +## Which n8n alternative is best for teams that don't want to self-host? + +Sim is the best n8n alternative for teams that want AI-agent workflows without operating the application infrastructure themselves. + +Managed hosting removes responsibility for application deployment, upgrades, availability, and routine infrastructure maintenance. Self-hosting provides more control, but it also makes the team responsible for securing, monitoring, upgrading, and recovering the deployment. + +Teams that do not want to self-host should compare whether each vendor offers a first-party managed service, how usage is metered, how credentials and run data are handled, whether governance features require a higher plan, and whether migration to or from self-hosting is practical. + +As of October 2026, Sim Cloud supports [workspace BYOK keys on every plan](https://docs.sim.ai/platform/costs#bring-your-own-key-byok). Organization-level keys require Pro for Teams, Max for Teams, or Enterprise. Teams that need local models can connect Ollama, vLLM, LM Studio, or LiteLLM on [any self-hosted Sim deployment](https://docs.sim.ai/platform/self-hosting); local models do not require Sim Enterprise. + +## How do n8n and Sim compare on licensing? + +Sim has a more permissively licensed core than n8n, while both products have licensing details that buyers should understand before adopting or commercializing a deployment. + +As of October 2026, n8n uses the [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license), a fair-code, source-available license that is not approved by the Open Source Initiative. The license allows many internal and personal uses but restricts uses such as commercially hosting n8n for third parties. + +As of October 2026, [Sim's core is licensed under Apache 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE), an OSI-approved open-source license. Code in `apps/sim/ee` is governed by the separate [Sim Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE); production use requires an active Sim Enterprise subscription, and modification and redistribution are not permitted. + +## Is n8n open source? + +n8n is source-available under the Sustainable Use License, but n8n is not open source under the Open Source Initiative's definition. + +The source code can be inspected and n8n can be self-hosted for permitted uses, but source visibility and self-hosting do not by themselves make a license OSI-approved. Teams planning to embed n8n, offer it to customers, or operate a commercial hosted service should review the [license terms](https://docs.n8n.io/privacy-and-security/sustainable-use-license) rather than assume that every self-hosted use is allowed. + +## Is Sim open source? + +Sim's core is open source under Apache 2.0, while the `apps/sim/ee` directory is covered by the separate Sim Enterprise License. + +That distinction matters when evaluating modification, redistribution, internal deployment, and production use of enterprise capabilities. Apache 2.0 applies to Sim's core, not automatically to every file in the repository. + +## How should teams compare n8n alternative pricing? + +Teams should compare n8n alternatives by total operating cost rather than by the lowest advertised monthly price. + +As of October 2026, buyers should use each vendor's official pricing page for current amounts and limits because hosted plan prices and usage allowances change. Compare hosted subscription charges, workflow or execution limits, model-token costs, premium connectors, collaboration features, enterprise governance, infrastructure, monitoring, upgrades, and staff time. + +Self-hosting is not automatically cheaper. A self-hosted deployment can reduce vendor charges or increase control, but the organization assumes infrastructure, security, backups, upgrades, incident response, and operational support. Managed hosting can cost less when engineering time is included. For n8n, compare the current execution model and plan limits on the [official pricing page](https://n8n.io/pricing/); Sim's [current pricing page](https://www.sim.ai/pricing) explains its plan and credit model. + +## What licensing questions should teams ask before choosing an n8n alternative? + +Teams should ask whether each n8n alternative is OSI-approved open source, source-available, or proprietary and whether its license permits the team's exact deployment and commercial model. + +1. Can the organization use the software in production without a commercial agreement? +2. Can the organization modify the source code? +3. Can the organization redistribute modified or unmodified versions? +4. Can the organization provide a hosted version to customers? +5. Are enterprise features stored under a different license from the core? +6. Do self-hosting rights change when the software is customer-facing? +7. Which legal entity and license version govern the code being deployed? + +License labels should not replace reading the actual license text. “Open,” “source-available,” “fair-code,” and “self-hostable” describe different legal arrangements. + +## What features matter when an n8n alternative handles AI agents? + +Sim and other AI-agent-oriented alternatives should be evaluated on model orchestration, state, tool use, approvals, evaluations, observability, and deployment rather than on connector count alone. + +A useful comparison examines multiple model providers and BYOK, tool calling, branching, human input, retrieval, traces, evaluations, reusable workflows, deployment choices, permissions, auditability, and credential controls. Sim's [Human in the Loop block](https://docs.sim.ai/workflows/blocks/human-in-the-loop) pauses a run and resumes it with submitted form fields; an approve-or-reject choice is a field, so a downstream Condition must inspect it. Sim's [Guardrails block](https://docs.sim.ai/workflows/blocks/guardrails) reports passed or failed, and a downstream Condition must route that result if the workflow should stop or take a different path. + +## How do you choose between Sim, n8n, Zapier, and Make? + +Sim is the strongest choice for AI-agent workflows, [n8n is a strong choice for extensible general automation](https://docs.n8n.io/), [Zapier is a strong choice for straightforward managed SaaS automation](https://zapier.com/pricing), and [Make is a strong choice for detailed visual process mapping](https://www.make.com/en/pricing). + +Choose Sim when AI agents, model flexibility, approvals, and deployment choice are central requirements. Choose n8n when general workflow automation and self-managed extensibility matter more than an AI-native workspace. Choose Zapier when the team wants a familiar managed service for connecting business applications. Choose Make when the team values granular visual control over deterministic scenarios. + +These categories overlap, so a proof of concept should use the same real workflow, data, approval step, failure case, and expected output in every shortlisted product. + +## When should a team stay with n8n? + +[n8n is the right incumbent to keep](https://docs.n8n.io/) when current workflows are reliable, its [license](https://docs.n8n.io/privacy-and-security/sustainable-use-license) fits the intended use, and migration would not create enough operational or AI-agent benefit to justify the cost. + +Existing expertise, tested integrations, established monitoring, and a large workflow estate all create legitimate switching costs. Migration becomes more compelling when the team needs a different license, wants managed hosting without its current maintenance burden, or increasingly builds agentic rather than deterministic workflows. + +## When should a team move from n8n to Sim? + +Sim is the stronger destination when AI-agent development, model flexibility, human input, and the licensing of the core matter more than preserving an existing n8n workflow estate. + +A safe migration starts with one representative workflow. Inventory triggers, credentials, API calls, data transformations, retries, approval steps, and failure paths; rebuild the workflow in Sim; test it with production-like inputs; compare outputs and operating cost; and deploy it gradually before retiring the original. + ## The 10 Best n8n Alternatives in 2026 +Sim, Make, Zapier, Activepieces, Pipedream, Gumloop, Lindy, Botpress, Microsoft Power Automate, and Workato are the ten alternatives compared below, with n8n retained as the incumbent baseline rather than counted as its own alternative. + ### Sim +Sim is the best overall alternative for teams building, deploying, and managing AI agents with a visual builder and flexible hosting. + **Best for:** People building AI agent workflows that require multi-model orchestration, persistent memory, and visual collaboration. [Sim](https://www.sim.ai) - is an open-source AI agent builder and visual workflow platform with [1,000+ integrations](https://www.sim.ai/pricing), as of September 2026. We support workflows built around agents, knowledge bases, tables, and multi-model routing. You can use Sim Cloud or deploy Sim with Docker Compose or Kubernetes. Both options support the same goal of building agent workflows without requiring you to manage infrastructure unless you choose to. + is the open-source AI workspace with [270+ integrations](https://www.sim.ai/pricing), as of October 2026. Sim supports workflows built around agents, knowledge bases, tables, and multi-model routing. You can use Sim Cloud or deploy Sim with Docker Compose or Kubernetes. Both options support the same goal of building agent workflows without requiring you to manage infrastructure unless you choose to. **Strengths:** -- **1,000+ integrations with multi-model LLM support, as of September 2026:** +- **270+ integrations with multi-model LLM support, as of October 2026:** [Connect supported model providers and integrations](https://www.sim.ai/pricing), including OpenAI, Claude, Gemini, Groq, Mistral, xAI, and local models via Ollama or vLLM. Model Context Protocol (MCP) support lets you connect compatible external tools in addition to Sim's prebuilt integrations. -- **Real-time collaboration and Sim Chat:** - Multiple people can edit workflows simultaneously and use comments and permissions to coordinate access. Sim Chat accepts natural-language instructions for planning and building workflows. +- **Real-time collaboration and Chat:** + Multiple people can edit workflows simultaneously and use comments and permissions to coordinate access. Chat accepts natural-language instructions for planning and building workflows. - **Managed and self-hosted deployment options:** Choose cloud-managed infrastructure with auto-scaling and built-in observability, or self-host via Docker Compose and Kubernetes. Self-hosting gives you control over the deployment and data environment, while Sim Cloud removes routine infrastructure management. - **Published security information:** Sim publishes its source code for review. Consult Sim's current security documentation for information about audits, controls, and compliance status. **Pricing:** - As of September 2026, Sim offers a free plan and paid plans with credit-based billing. Check [Sim's current pricing](https://www.sim.ai/pricing) for execution charges, storage allowances, and plan limits. + As of October 2026, [Sim offers a $0 Free plan and paid Pro, Max, and Enterprise plans with credit-based billing](https://www.sim.ai/pricing). **Limitations:** Sim is a newer platform, so some niche third-party integrations available in more mature tools like Zapier or Make may not yet exist. The templates library is growing but not yet as extensive as more established competitors. ### Make (formerly Integromat) +[Make is strongest](https://www.make.com/en/pricing) for teams that want managed visual automation with detailed control over deterministic scenarios. + **Best for:** People who want managed visual automation with detailed inspection of routes, filters, and intermediate values. @@ -244,18 +370,20 @@ Make displays routes, filters, and intermediate values in its visual scenario bu Make displays routers, filters, error handlers, and intermediate values in its scenario builder. The canvas can help you trace branching workflows and inspect data transformations. - **Strong data transformation and error handling:** Built-in functions for parsing, mapping, and reformatting data between apps mean fewer workarounds and manual steps compared to simpler tools. -- **3,000+ app integrations, as of September 2026:** - [Make lists 3,000+ apps](https://www.make.com/en/pricing), compared with [Zapier's 9,000+ apps](https://zapier.com/), and includes HTTP/webhook modules for custom connections. +- **3,500+ app integrations, as of October 2026:** + [Make lists 3,500+ apps](https://www.make.com/en/pricing), compared with [Zapier's 9,000+ apps](https://zapier.com/), and includes HTTP/webhook modules for custom connections. **Limitations:** Make provides AI modules and agent features, but availability and behavior vary by product and plan. Complex agent workflows in Make may require additional modules or services for persistent memory and multi-agent coordination. If you need those capabilities, compare Make's current agent features with Sim's native agent workflow support. **Pricing:** -As of September 2026, [Make](https://www.make.com/en/pricing) - offers a free plan with 1,000 credits per month. Paid plans scale by credit allowance and features. Make defines credit consumption by module and feature, so confirm how each step in your scenario is billed under the current plan. Test the same workflow in Make and Zapier to compare credit consumption with Zapier task usage. +As of October 2026, [Make](https://www.make.com/en/pricing) + offers a free plan with 1,000 credits per month. Paid plans scale by credit allowance and features. Make counts each module action, such as adding a spreadsheet row or fetching an email, as one credit, with separate rules for some AI features. Test the same workflow in Make and Zapier to compare credit consumption with Zapier task usage. ### Zapier +[Zapier is strongest](https://zapier.com/pricing) for non-technical teams that prioritize fast setup and a broad managed SaaS integration catalog. + **Best for:** Non-technical teams who need the widest integration catalog and the fastest time to first working automation. @@ -274,10 +402,12 @@ Zapier uses a linear builder for trigger-action automation across a large connec Zapier's task-based pricing can increase as execution volume grows. Its linear abstraction may also require additional steps for complex branching, loops, and data transformation. Complex branching, loops, and data transformations may require more steps in Zapier than in n8n or Make. Developers migrating code-heavy workflows should test those patterns before choosing Zapier. **Pricing:** - As of September 2026, Zapier offers a free plan with 100 tasks per month, while paid plans scale with task volume. Check [Zapier's pricing page](https://zapier.com/pricing) for current rates, as plan structures have shifted multiple times. + As of October 2026, [Zapier offers a free plan with 100 tasks per month and a Professional plan starting at $19.99 per month](https://zapier.com/pricing), while higher plans scale with task volume and collaboration features. ### Activepieces +[Activepieces is strongest](https://www.activepieces.com/pricing) for teams seeking visual automation with managed and MIT-licensed self-hosted options. + **Best for:** People who want a self-hostable alternative with an MIT-licensed Community Edition and cloud plans with published usage allowances. @@ -293,8 +423,7 @@ Activepieces is an n8n alternative for readers who prioritize permissive licensi - **Clean, accessible UI:** Activepieces provides a visual interface for building flows and code steps for custom logic. - **Cloud pricing:** -As of September 2026, [cloud pricing](https://www.activepieces.com/pricing) - includes a free plan and paid plans with usage allowances. Confirm current credits, included users, and plan features before choosing a cloud subscription. +As of October 2026, [Activepieces cloud pricing](https://www.activepieces.com/pricing) includes a free plan with 1,000 monthly credits and up to five users, a $20-per-month Plus plan, and a $200-per-month Team plan. **Limitations:** The connector ecosystem is smaller than n8n, Make, or Zapier. Enterprise features like SSO, audit logs, and custom RBAC are available under the commercial license, so fully free self-hosted deployments miss those governance capabilities. @@ -304,6 +433,8 @@ As of September 2026, [cloud pricing](https://www.activepieces.com/pricing) ### Pipedream +[Pipedream is strongest](https://pipedream.com/pricing) for developers who want code-capable API workflows on managed infrastructure. + **Best for:** Developers who want code-level control over automation without managing server infrastructure. @@ -322,10 +453,12 @@ As of September 2026, [cloud pricing](https://www.activepieces.com/pricing) The code-first approach means non-technical teammates can't build or modify workflows independently. Pipedream uses a step-based visual workflow interface rather than a free-form canvas, and it does not center the product on native AI agent orchestration. **Pricing:** - As of September 2026, [Pipedream's pricing page](https://pipedream.com/pricing) lists a free tier and paid plans metered in credits. Confirm the current credit allowances and plan features before choosing a plan. + As of October 2026, [Pipedream's pricing page](https://pipedream.com/pricing) lists a free development tier and a Startup plan at $99 per month when billed annually, including 10,000 usage credits. ### Gumloop +[Gumloop is strongest](https://www.gumloop.com/pricing) for non-developers assembling hosted, AI-powered business processes visually. + **Best for:** Non-developer teams building LLM-powered workflows with a visual canvas similar to n8n. @@ -344,10 +477,12 @@ Gumloop centers its visual canvas on AI models, prompts, and data sources rather Gumloop is a smaller platform with less mature enterprise governance features. Integration depth falls well below Make or Zapier for traditional SaaS connectors. If your workflows mix AI processing with heavy SaaS-to-SaaS automation, you may need to pair Gumloop with another tool. **Pricing:** - Verify current pricing on [Gumloop's website](https://www.gumloop.com/pricing). + As of October 2026, [Gumloop publishes its hosted plan and usage terms on its official pricing page](https://www.gumloop.com/pricing). ### Lindy +[Lindy is strongest](https://www.lindy.ai/pricing) for teams that want template-led agents for defined email, meeting, and sales tasks. + **Best for:** Less technical teams and individuals who want AI agents for specific use cases like email management, meeting scheduling, and sales outreach, with minimal setup overhead. @@ -366,11 +501,12 @@ Lindy provides preconfigured agents for tasks such as email management, meeting Credit-based pricing can become expensive at high automation volume, so estimate usage with a representative task before choosing a plan. Lindy is better for task-specific agents than complex multi-agent orchestration, and it's limited to its defined use case categories. If your workflow doesn't fit Lindy's templates, you may need a more configurable platform. **Pricing:** -As of September 2026, [Lindy's](https://www.lindy.ai/pricing) - Plus plan is $49.99/month, with Pro at $99.99 and Max at $199.99. There is a 7-day trial but no permanent free plan. +As of October 2026, Lindy presents two pricing structures on its official pages. Its [public team pricing page](https://www.lindy.ai/pricing) lists Free, Team from $49.99 per active user per month with 5,000 pooled monthly credits, and custom Enterprise plans. Its separate [teammate billing documentation](https://docs.lindy.ai/teammate/lindy-teammate-billing) lists seat-based Plus, Pro, and Max plans and says introductory free credits expire after seven days. ### Botpress +[Botpress is strongest](https://botpress.com/pricing) for teams building conversational agents for customer support, sales, chat, and voice. + **Best for:** Teams building AI agents specifically for chat and voice channels, including customer support bots, conversational sales agents, and multi-turn dialogue workflows. @@ -389,10 +525,12 @@ Botpress is designed specifically for conversational AI across chat and voice us Botpress is optimized for conversational agent use cases. If you need data pipeline orchestration, backend API automation, or internal operational workflows, this isn't the right tool. **Pricing:** - Verify current pricing on [Botpress's website](https://botpress.com/pricing). + As of October 2026, [Botpress offers a free entry plan and paid plans metered in included conversations](https://botpress.com/pricing). ### Microsoft Power Automate +[Microsoft Power Automate is strongest](https://www.microsoft.com/en-us/power-platform/products/power-automate/pricing) for organizations standardized on Microsoft 365, Dynamics, Azure, and desktop automation. + **Best for:** Organizations standardized on Microsoft 365, Dynamics, and the broader Microsoft ecosystem. @@ -408,22 +546,24 @@ Microsoft Power Automate offers first-party connections to SharePoint, Teams, Ou Data loss prevention policies, environment management, and audit logging come built in for organizations with strict governance requirements. **Limitations:** - It's a poor fit for teams outside the Microsoft ecosystem. AI Builder features add cost on top of base licensing. Simple automations may require more configuration than they do in narrower trigger-action tools. Licensing can also involve per-user, per-flow, and add-on charges, so confirm the required licenses for your deployment. + It's a poor fit for teams outside the Microsoft ecosystem. AI Builder features add cost on top of base licensing. Simple automations may require more configuration than they do in narrower trigger-action tools. Licensing can also involve per-user, per-flow, and add-on charges. **Pricing:** - As of September 2026, Microsoft offers a free trial and standalone Power Automate plans. Verify current per-user and per-bot pricing on [Microsoft's Power Automate pricing page](https://www.microsoft.com/en-us/power-platform/products/power-automate/pricing). + As of October 2026, [Microsoft offers a free trial and standalone Power Automate plans](https://www.microsoft.com/en-us/power-platform/products/power-automate/pricing), including Power Automate Process at $150 per bot per month and Hosted Process at $215 per bot per month when paid yearly. ### Workato +[Workato is strongest](https://docs.workato.com/en/pricing) for large organizations that need governed enterprise integration and contractual support. + **Best for:** Enterprise teams that need guaranteed SLAs, dedicated compliance certifications, and governance controls that n8n's self-managed model can't provide. -Workato is a managed enterprise integration platform for deployments that require procurement review, governance controls, and contractual support. Verify the scope of Workato's current audit reports, service commitments, and HIPAA-related terms before using it for a regulated workload. +Workato is a managed enterprise integration platform for deployments that require procurement review, governance controls, and contractual support. Contract terms determine the audit scope, service commitments, and regulated-workload coverage for a particular deployment. **Strengths:** - **Enterprise security and compliance controls:** - Workato publishes information about its compliance programs, certifications, and audit capabilities for procurement and security reviews. Confirm that the current scope covers your workload and region. + Workato provides compliance, certification, and audit information for procurement and security reviews, while each contract defines the applicable scope and region. - **Dedicated support:** Named account managers, priority support channels, and SLAs for issue resolution. - **Complex multi-system orchestration:** @@ -433,20 +573,20 @@ Workato is a managed enterprise integration platform for deployments that requir Priced for enterprise budgets. Workato is not suited for smaller teams or cost-sensitive deployments. Workato is proprietary, so migration may require rebuilding recipes, connections, and governance controls in another platform. Assess data portability and contract terms before deployment. **Pricing:** - As of September 2026, [Workato's pricing documentation](https://docs.workato.com/en/pricing) describes a usage-based model with platform edition and usage fees for direct customers. Workato also documents credit-based Free and Pro self-service plans; confirm which model and allowances apply to your account. + As of October 2026, [Workato's pricing documentation](https://docs.workato.com/en/pricing) describes a usage-based model with a platform edition, Workato credits, and optional add-ons. Its self-service Free plan includes a one-time 50,000-credit grant, while Pro offers recurring monthly credit allowances. ## How Open-Source Licenses Affect n8n Alternatives -The applicable software license determines how you may self-host, modify, and redistribute an n8n alternative. Review the license for the specific edition you plan to deploy because community and enterprise editions may use different terms. For a clause-by-clause look at what each model allows for SaaS hosting, white-labeling, and redistribution, see [Apache 2.0 vs fair-code](https://www.sim.ai/library/apache-2-0-vs-fair-code). +n8n, Activepieces, and Sim apply different licenses that determine how you may self-host, modify, and redistribute their software. Review the license for the specific edition you plan to deploy because community and enterprise editions may use different terms. For a clause-by-clause look at what each model allows for SaaS hosting, white-labeling, and redistribution, see [Apache 2.0 vs fair-code](https://www.sim.ai/library/apache-2-0-vs-fair-code). **n8n is source-available, not OSI-approved open source.** n8n makes its source code available under the [Sustainable Use License](https://docs.n8n.io/privacy-and-security/sustainable-use-license), which n8n describes as fair-code. The license permits internal use, modification, and some redistribution, but it restricts products whose value derives substantially from n8n's functionality, so it does not meet the [Open Source Definition](https://opensource.org/osd). Agencies, consultancies, and SaaS companies should review the license and seek legal advice before offering n8n functionality to customers. **Activepieces uses the MIT license.** - Activepieces distributes its Community Edition under the MIT license, which permits commercial use, modification, and redistribution when you preserve the required copyright and license notices. Confirm the license that applies to any enterprise features you use. + [Activepieces distributes its core under the MIT license and enterprise features under a Commercial License](https://www.activepieces.com/docs/about/license). The MIT license permits commercial use, modification, and redistribution when you preserve the required copyright and license notices. -**Sim uses the Apache License 2.0.** - Sim's core code is released under the OSI-approved [Apache License 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE) and can be self-hosted with Docker Compose or Kubernetes. Features in `apps/sim/ee` use a [separate Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE). +**Sim's core uses the Apache License 2.0.** + Sim's core code is released under the OSI-approved [Apache License 2.0](https://github.com/simstudioai/sim/blob/main/LICENSE) and can be self-hosted with Docker Compose or Kubernetes. Features in `apps/sim/ee` use a [separate Enterprise License](https://github.com/simstudioai/sim/blob/main/apps/sim/ee/LICENSE): production use requires an active Sim Enterprise subscription, and modification and redistribution are not permitted. **Migration effort varies with workflow complexity.** Moving a portfolio of workflows from n8n to any alternative requires real engineering effort. Migration effort grows with the number of workflows, custom code steps, credentials, external dependencies, and differences between execution models. Activepieces may preserve more trigger-action concepts, while Sim or Pipedream may require you to redesign state management, agent behavior, or code execution. @@ -456,17 +596,19 @@ The applicable software license determines how you may self-host, modify, and re ## The Bottom Line -Choose an n8n alternative based on the workflow type and operating model you need. +Sim ranks first for AI-agent workflows, while Make, Zapier, Activepieces, Pipedream, Gumloop, Lindy, Botpress, Microsoft Power Automate, and Workato remain stronger for their specific use cases. -- **Best for self-hosting:** Choose n8n for developer-controlled, traditional automation when your team is comfortable managing infrastructure. Choose Activepieces when an unrestricted MIT license is the priority, or Sim when you need self-hosted AI agent workflows under Apache 2.0. -- **Best for AI-native workflows:** As of September 2026, choose Sim for an open-source visual AI agent builder with [1,000+ integrations](https://www.sim.ai/pricing), native multi-model orchestration, agent memory, and MCP support. It also offers managed cloud infrastructure for teams that do not want to self-host. Choose Gumloop for a narrower, non-developer-focused LLM workflow canvas. +- **Best for self-hosting:** Choose n8n for developer-controlled, traditional automation when your team is comfortable managing infrastructure. Choose Activepieces when an unrestricted MIT license is the priority, or Sim when you need self-hosted AI agent workflows with an Apache 2.0 core and separately licensed enterprise code. +- **Best for AI-native workflows:** As of October 2026, choose Sim, the open-source AI workspace, for a visual builder with [270+ integrations](https://www.sim.ai/pricing), native multi-model orchestration, agent memory, and MCP support. It also offers managed cloud infrastructure for teams that do not want to self-host. Choose Gumloop for a narrower, non-developer-focused LLM workflow canvas. - **Best for small teams:** Choose Make for managed visual automation, strong debugging, and flexible data transformation. - **Best for non-technical users:** Choose Zapier for the fastest route to straightforward SaaS automation and the widest integration catalog. Choose Gumloop instead when AI is central to the workflow. -Use the comparison framework to narrow the list to two or three candidates, then build the same representative workflow in each one. Include at least one model call, one tool invocation, one conditional branch, one failure or retry, and one human approval so the test reveals operational differences rather than demo-level similarities. Sim, Make, Zapier, Activepieces, and Pipedream offer free tiers that may support an initial test. Confirm current limits before building a proof of concept. +Use the comparison framework to narrow the list to two or three candidates, then build the same representative workflow in each one. Include at least one model call, one tool invocation, one conditional branch, one failure or retry, and one human approval so the test reveals operational differences rather than demo-level similarities. Sim, Make, Zapier, Activepieces, and Pipedream offer free tiers that may support an initial test, with the October 2026 limits linked in their profiles above. ## Related comparisons +Sim buyers can use these related comparisons to evaluate licensing, architecture, and adjacent platform categories. + - [Sim vs n8n](https://www.sim.ai/comparisons/n8n): feature, pricing, security, and deployment details for the two platforms side by side. - [Sim vs n8n vs OpenAI AgentKit](https://www.sim.ai/library/openai-vs-n8n-vs-sim): how agent-first, automation-first, and OpenAI-native architectures differ. - [Apache 2.0 vs fair-code](https://www.sim.ai/library/apache-2-0-vs-fair-code): what each license allows for self-hosting and commercial products. diff --git a/apps/sim/ee/scim/lib/application/groups/manage-groups.test.ts b/apps/sim/ee/scim/lib/application/groups/manage-groups.test.ts index 982391eb525..7b469c7a426 100644 --- a/apps/sim/ee/scim/lib/application/groups/manage-groups.test.ts +++ b/apps/sim/ee/scim/lib/application/groups/manage-groups.test.ts @@ -94,6 +94,7 @@ describe('replaceScimGroup', () => { memberIds.delete(scimUserId) ) mocks.countMembers.mockImplementation(async () => memberIds.size) + mocks.reconcile.mockResolvedValue({ added: [], removed: [], raised: [], stoppedRuns: [] }) mocks.touch.mockImplementation(async () => { group = { ...group, updatedAt: new Date('2026-01-02T00:00:00.000Z') } }) diff --git a/apps/sim/ee/scim/lib/application/groups/manage-groups.ts b/apps/sim/ee/scim/lib/application/groups/manage-groups.ts index 16a303b2871..2ad3f480e59 100644 --- a/apps/sim/ee/scim/lib/application/groups/manage-groups.ts +++ b/apps/sim/ee/scim/lib/application/groups/manage-groups.ts @@ -5,6 +5,8 @@ import { and, eq, ne } from 'drizzle-orm' import type { ScimPatchOperation } from '@/lib/api/contracts/scim' import { acquireOrganizationMutationLock } from '@/lib/billing/organizations/membership' import type { DbOrTx, DbTransaction } from '@/lib/db/types' +import type { StoppedRun } from '@/lib/mothership/async-runs/repository' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { defineAuthorizedScimUseCase, type ScimUseCaseArgs, @@ -50,16 +52,19 @@ import { /** * Every group write runs in one transaction under the organization lock, which * heads the documented lock order, so two full-membership PATCHes cannot both - * compute from the same stale membership and keep members from both. + * compute from the same stale membership and keep members from both. The runs + * its projection stopped are delivered once that transaction commits. */ -function withGroupWrite( +async function withGroupWrite( context: ScimUseCaseContext, work: (tx: DbTransaction) => Promise -): Promise { - return db.transaction(async (tx) => { +): Promise> { + const { stoppedRuns, ...result } = await db.transaction(async (tx) => { await acquireOrganizationMutationLock(tx, context.organizationId) return work(tx) }) + deliverRunStops(stoppedRuns) + return result } async function assertDisplayNameAvailable( @@ -205,7 +210,7 @@ export const createScimGroup = defineAuthorizedScimUseCase({ }) : 'no-match' - await reconcileUsersProjection(tx, { + const projection = await reconcileUsersProjection(tx, { connectionId: context.connection.id, organizationId: context.organizationId, scimUserIds: memberIds, @@ -225,6 +230,7 @@ export const createScimGroup = defineAuthorizedScimUseCase({ resource: toGroupResource({ ...created, members }, context.baseUrl), touchedUserIds: memberIds, renamed: true, + stoppedRuns: projection.stoppedRuns, } }) }, @@ -295,7 +301,7 @@ export const replaceScimGroup = defineAuthorizedScimUseCase({ await assertMemberCount(tx, current.id) if (touched.size > 0) await touchScimGroup(tx, current.id) - await reconcileUsersProjection(tx, { + const projection = await reconcileUsersProjection(tx, { connectionId: context.connection.id, organizationId: context.organizationId, scimUserIds: [...touched], @@ -317,6 +323,7 @@ export const replaceScimGroup = defineAuthorizedScimUseCase({ resource: toGroupResource({ ...refreshed, members }, context.baseUrl), touchedUserIds: [...touched], renamed: renamed || (current.externalId ?? null) !== (refreshed.externalId ?? null), + stoppedRuns: projection.stoppedRuns, } }) }, @@ -428,7 +435,7 @@ export const patchScimGroup = defineAuthorizedScimUseCase({ await assertMemberCount(tx, current.id) if (touched.size > 0) await touchScimGroup(tx, current.id) - await reconcileUsersProjection(tx, { + const projection = await reconcileUsersProjection(tx, { connectionId: context.connection.id, organizationId: context.organizationId, scimUserIds: [...touched], @@ -447,6 +454,7 @@ export const patchScimGroup = defineAuthorizedScimUseCase({ patch.kind === 'full' ? (patch.displayName ?? current.displayName) : current.displayName, touchedUserIds: [...touched], renamed, + stoppedRuns: projection.stoppedRuns, } }) }, @@ -481,7 +489,7 @@ export const deleteScimGroup = defineAuthorizedScimUseCase({ */ const memberIds = await loadGroupMemberIds(tx, current.id) await deleteScimGroupRow(tx, current.id) - await reconcileUsersProjection(tx, { + const projection = await reconcileUsersProjection(tx, { connectionId: context.connection.id, organizationId: context.organizationId, scimUserIds: memberIds, @@ -491,6 +499,7 @@ export const deleteScimGroup = defineAuthorizedScimUseCase({ groupId: current.id, displayName: current.displayName, memberCount: memberIds.length, + stoppedRuns: projection.stoppedRuns, } }) }, diff --git a/apps/sim/ee/scim/lib/application/users/deprovision-user.ts b/apps/sim/ee/scim/lib/application/users/deprovision-user.ts index 602db059e66..6af018da930 100644 --- a/apps/sim/ee/scim/lib/application/users/deprovision-user.ts +++ b/apps/sim/ee/scim/lib/application/users/deprovision-user.ts @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { removeUserFromOrganization } from '@/lib/billing/organizations/membership' import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { defineAuthorizedScimUseCase, type ScimUseCaseArgs, @@ -74,6 +75,7 @@ export const deprovisionScimUser = defineAuthorizedScimUseCase({ memberId: membership.id, revokePersonalApiKeys: true, }) + deliverRunStops(removal.stoppedRuns) if (!removal.success) { throw new ScimError(409, undefined, removal.error ?? 'The member could not be removed') } diff --git a/apps/sim/ee/scim/lib/application/users/provision-user.ts b/apps/sim/ee/scim/lib/application/users/provision-user.ts index 08523eee734..17fc69a3b63 100644 --- a/apps/sim/ee/scim/lib/application/users/provision-user.ts +++ b/apps/sim/ee/scim/lib/application/users/provision-user.ts @@ -215,6 +215,7 @@ export const provisionScimUser = defineAuthorizedScimUseCase({ connectionId: context.connection.id, externalId: attributes.externalId, }) + /** The new directory record has no provenance to withdraw, so this stops no runs. */ await reconcileUserProjection(tx, { connectionId: context.connection.id, organizationId: context.organizationId, diff --git a/apps/sim/ee/scim/lib/application/users/update-user.test.ts b/apps/sim/ee/scim/lib/application/users/update-user.test.ts index 43d9ce514a2..b809b8a8495 100644 --- a/apps/sim/ee/scim/lib/application/users/update-user.test.ts +++ b/apps/sim/ee/scim/lib/application/users/update-user.test.ts @@ -133,7 +133,7 @@ describe('user updates', () => { beforeEach(() => { resetDbChainMock() mocks.loadGroups.mockResolvedValue(new Map()) - mocks.reconcile.mockResolvedValue({ added: [], removed: [], raised: [] }) + mocks.reconcile.mockResolvedValue({ added: [], removed: [], raised: [], stoppedRuns: [] }) }) it('serializes on the organization and user locks before computing the update', async () => { diff --git a/apps/sim/ee/scim/lib/application/users/update-user.ts b/apps/sim/ee/scim/lib/application/users/update-user.ts index 900f814a5be..471e93b2deb 100644 --- a/apps/sim/ee/scim/lib/application/users/update-user.ts +++ b/apps/sim/ee/scim/lib/application/users/update-user.ts @@ -5,6 +5,8 @@ import { normalizeEmail } from '@sim/utils/string' import type { ScimPatchOperation } from '@/lib/api/contracts/scim' import { acquireOrganizationUserMutationLocks } from '@/lib/billing/organizations/membership' import type { DbOrTx, DbTransaction } from '@/lib/db/types' +import type { StoppedRun } from '@/lib/mothership/async-runs/repository' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { suspendMemberTx, unsuspendMemberTx } from '@/lib/organizations/members/lifecycle' import { invalidateAfterSessionRevocation, @@ -42,18 +44,24 @@ import { * deactivation actually does. */ -export interface UpdateOutcome { +interface UpdateOutcome { emailChanged: boolean deactivated: boolean reactivated: boolean } +/** An applied update and the runs its projection stopped, delivered once the write commits. */ +interface AppliedUserUpdate { + outcome: UpdateOutcome + stoppedRuns: StoppedRun[] +} + async function applyUserUpdate( tx: DbTransaction, context: ScimUseCaseContext, current: ScimUserRecord, next: ScimUserAttributes -): Promise { +): Promise { const nextEmail = primaryEmail(next) const nextName = accountName(next) const emailChanged = accountEmailDiverged(current, next) @@ -108,14 +116,17 @@ async function applyUserUpdate( active: next.active, }) - await reconcileUserProjection(tx, { + const projection = await reconcileUserProjection(tx, { connectionId: context.connection.id, organizationId: context.organizationId, scimUserId: current.id, settings: context.connection.settings, }) - return { emailChanged, deactivated, reactivated } + return { + outcome: { emailChanged, deactivated, reactivated }, + stoppedRuns: projection.stoppedRuns, + } } export interface UpdateScimUserResult { @@ -125,6 +136,27 @@ export interface UpdateScimUserResult { resource: ReturnType } +/** Runs a user write in one transaction and delivers the runs it stopped once that commits. */ +async function writeUser( + context: ScimUseCaseContext, + scimUserId: string, + update: (tx: DbTransaction, current: ScimUserRecord) => Promise +): Promise { + const { stoppedRuns, ...result } = await db.transaction(async (tx) => { + const current = await loadUserForUpdate(tx, context, scimUserId) + const applied = await update(tx, current) + return { + scimUserId: current.id, + userId: current.userId, + outcome: applied?.outcome ?? null, + resource: await renderUpdated(tx, context.connection.id, current.id, context.baseUrl), + stoppedRuns: applied?.stoppedRuns ?? [], + } + }) + deliverRunStops(stoppedRuns) + return result +} + /** * Loads the stored resource under the organization and user advisory locks. * @@ -229,9 +261,7 @@ export const replaceScimUser = defineAuthorizedScimUseCase({ input, context, }: ScimUseCaseArgs): Promise { - return db.transaction(async (tx) => { - const current = await loadUserForUpdate(tx, context, input.scimUserId) - + return writeUser(context, input.scimUserId, async (tx, current) => { /** * A replace keeps attributes Sim does not model that the directory sent on * a previous write but omitted now, so a partial mapping does not erase @@ -249,16 +279,10 @@ export const replaceScimUser = defineAuthorizedScimUseCase({ * changes nothing must not write, audit, or re-project, or a 2,000-user * organization produces 2,000 spurious audit rows per sync. */ - const outcome = - userAttributesEqual(current.attributes, next) && !accountIdentityDiverged(current, next) - ? null - : await applyUserUpdate(tx, context, current, next) - return { - scimUserId: current.id, - userId: current.userId, - outcome, - resource: await renderUpdated(tx, context.connection.id, current.id, context.baseUrl), - } + return userAttributesEqual(current.attributes, next) && + !accountIdentityDiverged(current, next) + ? null + : applyUserUpdate(tx, context, current, next) }) }, projectAudit: ({ result }) => auditEntries(result), @@ -277,9 +301,7 @@ export const patchScimUser = defineAuthorizedScimUseCase({ input, context, }: ScimUseCaseArgs): Promise { - return db.transaction(async (tx) => { - const current = await loadUserForUpdate(tx, context, input.scimUserId) - + return writeUser(context, input.scimUserId, async (tx, current) => { const { next, changed } = applyUserPatch(current.attributes, input.operations) /** @@ -289,16 +311,9 @@ export const patchScimUser = defineAuthorizedScimUseCase({ * projection pass, and a `lastModified` bump for a request that meant * nothing. */ - const outcome = - changed || accountIdentityDiverged(current, next) - ? await applyUserUpdate(tx, context, current, next) - : null - return { - scimUserId: current.id, - userId: current.userId, - outcome, - resource: await renderUpdated(tx, context.connection.id, current.id, context.baseUrl), - } + return changed || accountIdentityDiverged(current, next) + ? applyUserUpdate(tx, context, current, next) + : null }) }, projectAudit: ({ result }) => auditEntries(result), diff --git a/apps/sim/ee/scim/lib/projection/reconcile-user.test.ts b/apps/sim/ee/scim/lib/projection/reconcile-user.test.ts index c1a8706339c..2a03b1d107e 100644 --- a/apps/sim/ee/scim/lib/projection/reconcile-user.test.ts +++ b/apps/sim/ee/scim/lib/projection/reconcile-user.test.ts @@ -23,6 +23,7 @@ const hoistedMocks = vi.hoisted(() => ({ lowerWorkspace: vi.fn(), readPermission: vi.fn(), revokeWorkspace: vi.fn(), + stopRuns: vi.fn(), })) vi.mock('@/lib/billing/organizations/membership', () => organizationMembershipMock) @@ -42,6 +43,7 @@ vi.mock('@/lib/workspaces/access/workspace-access', () => ({ lowerWorkspaceAccessTx: hoistedMocks.lowerWorkspace, readWorkspacePermission: hoistedMocks.readPermission, revokeWorkspaceAccessTx: hoistedMocks.revokeWorkspace, + stopRunsWithoutWorkspaceAccessTx: hoistedMocks.stopRuns, permissionRank: (permission: string) => ({ read: 1, write: 2, admin: 3 })[permission] ?? 0, })) @@ -117,10 +119,20 @@ describe('reconcileUserProjection', () => { mocks.grantWorkspace.mockResolvedValue('granted') mocks.lowerWorkspace.mockResolvedValue('lowered') mocks.readPermission.mockResolvedValue('write') - mocks.revokeWorkspace.mockResolvedValue({ revoked: true, ownershipTransferred: false }) + mocks.revokeWorkspace.mockResolvedValue({ + revoked: true, + ownershipTransferred: false, + stoppedRuns: [], + }) + mocks.stopRuns.mockResolvedValue([]) mocks.addMember.mockResolvedValue('added') mocks.removeMember.mockResolvedValue('removed') - mocks.changeMemberRole.mockResolvedValue({ changed: true, from: 'member', to: 'admin' }) + mocks.changeMemberRole.mockResolvedValue({ + changed: true, + from: 'member', + to: 'admin', + stoppedRuns: [], + }) }) it('takes the organization and user locks before reading any grant', async () => { @@ -198,6 +210,7 @@ describe('reconcileUserProjection', () => { organizationId: 'org-1', userId: 'u-1', role: 'admin', + deferRunStops: true, }) }) @@ -301,7 +314,11 @@ describe('reconcileUserProjection', () => { vi.clearAllMocks() resetDbChainMock() - mocks.revokeWorkspace.mockResolvedValue({ revoked: true, ownershipTransferred: false }) + mocks.revokeWorkspace.mockResolvedValue({ + revoked: true, + ownershipTransferred: false, + stoppedRuns: [], + }) mocks.readPermission.mockResolvedValue('write') stage({ current: [ diff --git a/apps/sim/ee/scim/lib/projection/reconcile-user.ts b/apps/sim/ee/scim/lib/projection/reconcile-user.ts index 696f4290945..15be39369c5 100644 --- a/apps/sim/ee/scim/lib/projection/reconcile-user.ts +++ b/apps/sim/ee/scim/lib/projection/reconcile-user.ts @@ -17,6 +17,8 @@ import { and, eq, inArray } from 'drizzle-orm' import { acquireOrganizationUserMutationLocks } from '@/lib/billing/organizations/membership' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbOrTx, DbTransaction } from '@/lib/db/types' +import type { StoppedRun } from '@/lib/mothership/async-runs/repository' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { changeMemberRoleTx } from '@/lib/organizations/members/lifecycle' import { addPermissionGroupMemberTx, @@ -31,6 +33,7 @@ import { permissionRank, readWorkspacePermission, revokeWorkspaceAccessTx, + stopRunsWithoutWorkspaceAccessTx, } from '@/lib/workspaces/access/workspace-access' import { type MappingRow, @@ -58,9 +61,24 @@ export interface ProjectionDelta { removed: ProjectionGrant[] /** Workspace grants whose level changed in either direction. */ raised: ProjectionGrant[] + /** + * Chat runs the withdrawals stopped in the caller's transaction. Whoever owns that transaction + * delivers their Stop with `deliverRunStops` once it commits. + */ + stoppedRuns: StoppedRun[] +} + +function emptyDelta(): ProjectionDelta { + return { added: [], removed: [], raised: [], stoppedRuns: [] } } -const EMPTY_DELTA: ProjectionDelta = { added: [], removed: [], raised: [] } +type ProjectionChanges = Omit + +function addChanges(total: ProjectionChanges, changes: ProjectionChanges): void { + total.added.push(...changes.added) + total.removed.push(...changes.removed) + total.raised.push(...changes.raised) +} /** Users per transaction when projecting outside a request's own transaction; the organization lock is held for the batch. */ export const PROJECTION_BATCH_SIZE = 25 @@ -265,7 +283,12 @@ async function setOrganizationRole( role: 'admin' | 'member' ): Promise { try { - const change = await changeMemberRoleTx(tx, { organizationId, userId, role }) + const change = await changeMemberRoleTx(tx, { + organizationId, + userId, + role, + deferRunStops: true, + }) return change.changed ? 'applied' : 'unchanged' } catch (error) { if (error instanceof OrchestrationError && error.code === 'conflict') { @@ -284,9 +307,10 @@ async function setOrganizationRole( } /** - * Withdraws one grant. Returns false when the grant must stay in place — the - * access could not be handed on — so the provenance row survives and the next - * pass retries instead of forgetting. + * Withdraws one grant, returning the workspaces where it may have ended the + * user's access. Returns null when the grant must stay in place — the access + * could not be handed on — so the provenance row survives and the next pass + * retries instead of forgetting. * * Adopted access — held by hand before any mapping covered it — is the person's * own, so the directory only forgets its record of it, unless the organization @@ -301,13 +325,13 @@ async function withdrawGrant( lockManualMembership: boolean foreignWorkspaceIds: Set } -): Promise { +): Promise { const { grant } = params - if (grant.origin === 'adopted' && !params.lockManualMembership) return true + if (grant.origin === 'adopted' && !params.lockManualMembership) return [] switch (grant.targetKind) { case 'workspace': { /** The workspace belongs to another tenant now; its access is theirs to manage. Forget the grant. */ - if (params.foreignWorkspaceIds.has(grant.targetId)) return true + if (params.foreignWorkspaceIds.has(grant.targetId)) return [] /** * A grant raised by hand above what the directory set is left alone. The * directory said "at least write"; someone deliberately made it admin, and @@ -318,13 +342,13 @@ async function withdrawGrant( workspaceId: grant.targetId, userId: params.userId, }) - if (!current) return true + if (!current) return [] if ( !params.lockManualMembership && grant.permissionType && permissionRank(current) > permissionRank(grant.permissionType) ) { - return true + return [] } if (!params.lockManualMembership && grant.baselinePermission) { if (permissionRank(current) > permissionRank(grant.baselinePermission)) { @@ -335,11 +359,12 @@ async function withdrawGrant( to: grant.baselinePermission, }) } - return true + return [] } const outcome = await revokeWorkspaceAccessTx(tx, { workspaceId: grant.targetId, userId: params.userId, + deferRunStops: true, }) if (!outcome.revoked) { logger.warn('Left workspace access in place: ownership could not be handed on', { @@ -347,14 +372,21 @@ async function withdrawGrant( userId: params.userId, reason: outcome.reason, }) - return false + return null } - return true + return [grant.targetId] + } + case 'org_role': { + if (grant.targetId !== 'admin') return [] + const demoted = await setOrganizationRole(tx, params.organizationId, params.userId, 'member') + if (demoted !== 'applied') return [] + /** An organization admin reaches every organization workspace; the demotion may end any of them. */ + const workspaces = await tx + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.organizationId, params.organizationId)) + return workspaces.map(({ id }) => id) } - case 'org_role': - if (grant.targetId !== 'admin') return true - await setOrganizationRole(tx, params.organizationId, params.userId, 'member') - return true case 'permission_group': try { await removePermissionGroupMemberTx(tx, { @@ -368,7 +400,7 @@ async function withdrawGrant( * target column carries no foreign key. Withdrawing from nothing is * complete, not a failure. */ - if (error instanceof PermissionGroupNotFoundError) return true + if (error instanceof PermissionGroupNotFoundError) return [] /** * An administrator moved the group back to governing everyone, and its * last member cannot leave without emptying it. The grant stays on @@ -381,11 +413,11 @@ async function withdrawGrant( userId: params.userId, conflict: error.message, }) - return false + return null } throw error } - return true + return [] } } @@ -412,7 +444,7 @@ export async function reconcileUserProjection( .where(and(eq(scimUser.id, params.scimUserId), eq(scimUser.connectionId, params.connectionId))) .limit(1) - if (!record) return EMPTY_DELTA + if (!record) return emptyDelta() /** * Taken before any target is touched. The organization lock is the root of @@ -450,19 +482,20 @@ export async function reconcileUserProjection( }) const plan = planGrantChanges(desired, current, actual) - const delta: ProjectionDelta = { added: [], removed: [], raised: [] } + const delta = emptyDelta() + const accessEndedIn = new Set() const lockManualMembership = params.settings.lockManualMembership === true /** Withdrawals first, so a move between groups frees its workspace slot. */ for (const grant of plan.withdraw) { - const withdrawn = await withdrawGrant(tx, { + const accessEnded = await withdrawGrant(tx, { organizationId: params.organizationId, userId: record.userId, grant, lockManualMembership, foreignWorkspaceIds, }) - if (!withdrawn) continue + if (!accessEnded) continue await tx .delete(scimProjectionGrant) .where( @@ -473,6 +506,7 @@ export async function reconcileUserProjection( ) ) delta.removed.push(grant) + for (const workspaceId of accessEnded) accessEndedIn.add(workspaceId) } for (const { grant, previousGrant } of plan.apply) { @@ -571,6 +605,12 @@ export async function reconcileUserProjection( else delta.added.push(grant) } + /** + * Runs are judged once every withdrawal and grant above is in place: a run in + * a workspace this pass takes away and gives back, by grant or by role, keeps + * going. + */ + delta.stoppedRuns = await stopRunsWithoutWorkspaceAccessTx(tx, record.userId, [...accessEndedIn]) return delta } @@ -583,15 +623,19 @@ export async function reconcileUsersProjection( scimUserIds: string[] settings: ScimConnectionSettings } -): Promise { +): Promise { + const total = emptyDelta() for (const scimUserId of [...new Set(params.scimUserIds)].sort()) { - await reconcileUserProjection(tx, { + const delta = await reconcileUserProjection(tx, { connectionId: params.connectionId, organizationId: params.organizationId, scimUserId, settings: params.settings, }) + addChanges(total, delta) + total.stoppedRuns.push(...delta.stoppedRuns) } + return total } /** @@ -601,30 +645,27 @@ export async function reconcileUsersProjection( * and an administrator changing a mapping on a large group. One transaction over * thousands of users would hold the organization's advisory locks for its whole * duration, blocking every invitation and role change in the tenant meanwhile. + * + * Each batch's stopped runs are delivered once that batch commits, so a later + * batch that fails neither loses an earlier batch's Stops nor delivers its own. */ export async function reconcileUsersProjectionInBatches(params: { connectionId: string organizationId: string scimUserIds: string[] settings: ScimConnectionSettings -}): Promise { - const total: ProjectionDelta = { added: [], removed: [], raised: [] } +}): Promise { + const changes: ProjectionChanges = { added: [], removed: [], raised: [] } const ids = [...new Set(params.scimUserIds)].sort() for (let start = 0; start < ids.length; start += PROJECTION_BATCH_SIZE) { - const batch = ids.slice(start, start + PROJECTION_BATCH_SIZE) - await db.transaction(async (tx) => { - for (const scimUserId of batch) { - const delta = await reconcileUserProjection(tx, { - connectionId: params.connectionId, - organizationId: params.organizationId, - scimUserId, - settings: params.settings, - }) - total.added.push(...delta.added) - total.removed.push(...delta.removed) - total.raised.push(...delta.raised) - } - }) + const { stoppedRuns, ...batch } = await db.transaction((tx) => + reconcileUsersProjection(tx, { + ...params, + scimUserIds: ids.slice(start, start + PROJECTION_BATCH_SIZE), + }) + ) + deliverRunStops(stoppedRuns) + addChanges(changes, batch) } - return total + return changes } diff --git a/apps/sim/hooks/queries/mothership-chats.ts b/apps/sim/hooks/queries/mothership-chats.ts index d18f4b2a317..a97408d6d87 100644 --- a/apps/sim/hooks/queries/mothership-chats.ts +++ b/apps/sim/hooks/queries/mothership-chats.ts @@ -370,29 +370,12 @@ async function readMothershipChatHistory( windowStartCursor: string | null | undefined, signal?: AbortSignal ): Promise { - try { - const data = await requestJson(getMothershipChatContract, { - params: { chatId }, - query: windowStartCursor ? { from: windowStartCursor } : { window: 'latest' }, - signal, - }) - return parseChatHistory(data) - } catch (error) { - if (!isApiClientError(error) || error.status !== 404) throw error - } - - // boundary-raw-fetch: legacy alias path /api/mothership/chat?chatId=... returns the - // copilot lifecycle shape (activeStreamId, not conversationId) for chats stored under - // the older copilot table; no contract exists for this alias path - const copilotRes = await fetch(`/api/mothership/chat?chatId=${encodeURIComponent(chatId)}`, { + const data = await requestJson(getMothershipChatContract, { + params: { chatId }, + query: windowStartCursor ? { from: windowStartCursor } : { window: 'latest' }, signal, }) - - if (!copilotRes.ok) { - throw new Error('Failed to load chat') - } - - return parseChatHistory(await copilotRes.json()) + return parseChatHistory(data) } /** diff --git a/apps/sim/lib/admin/dashboard.ts b/apps/sim/lib/admin/dashboard.ts index a81cc0af77b..6865a6be4ff 100644 --- a/apps/sim/lib/admin/dashboard.ts +++ b/apps/sim/lib/admin/dashboard.ts @@ -77,6 +77,7 @@ import { env } from '@/lib/core/config/env' import { executeTransactionallyIdempotent } from '@/lib/core/idempotency/transaction' import { enqueueOutboxEvent } from '@/lib/core/outbox/service' import type { DbOrTx } from '@/lib/db/types' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { ownedAttachableWorkspacesWhere } from '@/lib/workspaces/organization-workspaces' interface PaginationInput { @@ -1781,6 +1782,7 @@ export async function removeDashboardOrganizationMember( organizationId, memberId, }) + deliverRunStops(result.stoppedRuns) if (!result.success) throw new Error(result.error ?? 'Failed to remove member') try { await reconcileOrganizationSeats({ diff --git a/apps/sim/lib/admin/member-operation.ts b/apps/sim/lib/admin/member-operation.ts index 6f2008b120b..e692c00d8e0 100644 --- a/apps/sim/lib/admin/member-operation.ts +++ b/apps/sim/lib/admin/member-operation.ts @@ -24,6 +24,7 @@ import { outboxPayloadHasSourceOperationId, } from '@/lib/core/outbox/service' import type { DbOrTx } from '@/lib/db/types' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { moveWorkspaceToOrganization } from '@/lib/workspaces/admin-move' import { MIGRATED_INVITATION_EMAIL_EVENT_TYPE } from '@/lib/workspaces/admin-move-event' import { ownedAttachableWorkspacesWhere } from '@/lib/workspaces/organization-workspaces' @@ -514,6 +515,7 @@ async function applyMembership( usageLimitDollars: request.usageLimitDollars, setBy: request.actor.id ?? undefined, }) + deliverRunStops(transferred.stoppedRuns) if (!transferred.success || !transferred.memberId) { throw new Error(transferred.error ?? 'Failed to transfer organization member') } diff --git a/apps/sim/lib/api/contracts/copilot.ts b/apps/sim/lib/api/contracts/copilot.ts index ec3057c5a43..72a5e3fbb27 100644 --- a/apps/sim/lib/api/contracts/copilot.ts +++ b/apps/sim/lib/api/contracts/copilot.ts @@ -186,14 +186,6 @@ export const copilotToolExecuteInternalBodySchema = z ) export type CopilotToolExecuteInternalBody = z.input -export const copilotChatGetQuerySchema = z - .object({ - workflowId: z.string().optional(), - workspaceId: z.string().optional(), - chatId: z.string().optional(), - }) - .passthrough() - export const copilotChatStreamQuerySchema = z.object({ streamId: z.string().optional().default(''), after: z.string().optional().default(''), diff --git a/apps/sim/lib/api/contracts/mothership-assistant-tools.ts b/apps/sim/lib/api/contracts/mothership-assistant-tools.ts index 60a9db7177d..50e5cc740c0 100644 --- a/apps/sim/lib/api/contracts/mothership-assistant-tools.ts +++ b/apps/sim/lib/api/contracts/mothership-assistant-tools.ts @@ -404,12 +404,26 @@ export const listWorkspacesResultSchema = z.object({ nextCursor: z.uuid().nullable(), storedNotes: z .array( - z.object({ - id: z.string(), - kind: z.enum(['user_pref', 'workspace_fact']), - content: z.string(), - updatedAt: z.iso.datetime(), - }) + z + .object({ + id: z.string(), + content: z.string(), + updatedAt: z.iso.datetime(), + by: z.enum(['this user', 'another workspace member', 'another user']).optional(), + chat: z.enum(['this chat', 'another chat']).optional(), + }) + .and( + z.union([ + z.object({ kind: z.enum(['user_pref', 'workspace_fact']) }), + z.object({ + scope: z.enum(['personal', 'project', 'workspace']), + visibility: z.enum(['private', 'shared']), + projectId: z.uuid().nullable(), + workspaceId: z.uuid().nullable(), + revision: z.number().int().positive(), + }), + ]) + ) ) .optional(), }) diff --git a/apps/sim/lib/api/contracts/mothership-chats.ts b/apps/sim/lib/api/contracts/mothership-chats.ts index 8d5de94df52..3fccdbf253d 100644 --- a/apps/sim/lib/api/contracts/mothership-chats.ts +++ b/apps/sim/lib/api/contracts/mothership-chats.ts @@ -168,15 +168,6 @@ export type MothershipExecuteBody = z.input export const mothershipEventsQuerySchema = mothershipChatOwnerSchema -export const mothershipChatGetQuerySchema = z - .object({ - workflowId: z.string().optional(), - workspaceId: z.string().optional(), - organizationId: z.string().optional(), - chatId: z.string().optional(), - }) - .passthrough() - export const mothershipChatPostEnvelopeSchema = z .object({ message: z.string().max(MAX_CHAT_MESSAGE_LENGTH).optional(), diff --git a/apps/sim/lib/api/contracts/mothership-memories.ts b/apps/sim/lib/api/contracts/mothership-memories.ts new file mode 100644 index 00000000000..4e05c298fe4 --- /dev/null +++ b/apps/sim/lib/api/contracts/mothership-memories.ts @@ -0,0 +1,12 @@ +import { defineRouteContract } from '@/lib/api/contracts/types' +import { + MemoryNotesAccessInput, + MemoryNotesAccessResult, +} from '@/lib/mothership/generated/memory-notes-access' + +export const resolveMemoryNotesAccessContract = defineRouteContract({ + method: 'POST', + path: '/api/mothership/memories/access', + body: MemoryNotesAccessInput, + response: { mode: 'json', schema: MemoryNotesAccessResult }, +}) diff --git a/apps/sim/lib/billing/core/mid-run-usage.ts b/apps/sim/lib/billing/core/mid-run-usage.ts index 3db0c1d342d..cae9c27e6e0 100644 --- a/apps/sim/lib/billing/core/mid-run-usage.ts +++ b/apps/sim/lib/billing/core/mid-run-usage.ts @@ -17,6 +17,7 @@ import { checkExecutionUsageLimits, USAGE_GATE_SETTLE_TIMEOUT_MS, USAGE_GATE_TTL_MS, + usageAdmissionTtlMs, } from '@/lib/billing/core/usage-gate-cache' import type { UsageUpgradePayer } from '@/lib/billing/usage-upgrade' import { coalesceLocally } from '@/lib/concurrency/singleflight' @@ -141,7 +142,7 @@ export async function readMidRunUsageVerdict( } /** - * Admitted direct-v1 verdicts, served for the execution gate's TTL like + * Admitted direct-v1 verdicts, served for {@link usageAdmissionTtlMs} like * {@link checkExecutionUsageLimits} serves attributed ones: the worker re-validates a run on * every resume leg, and each uncached read sums the payer's ledger for the period. Only a * `within` verdict is stored, so a refusal or an unreadable ledger is always read again. @@ -227,7 +228,7 @@ export async function readMidRunAccountUsageVerdict( } } const within: MidRunUsageVerdict = { status: 'within' } - accountVerdictCache.set(key, within) + accountVerdictCache.set(key, within, { ttl: usageAdmissionTtlMs(usage) }) return within } catch (error) { logger.warn('Mid-run account usage read failed; continuing the run', { diff --git a/apps/sim/lib/billing/core/reporting-usage-cache.integration.ts b/apps/sim/lib/billing/core/reporting-usage-cache.integration.ts index e83bed744cf..062c9de0f98 100644 --- a/apps/sim/lib/billing/core/reporting-usage-cache.integration.ts +++ b/apps/sim/lib/billing/core/reporting-usage-cache.integration.ts @@ -186,6 +186,10 @@ describe.runIf(Boolean(redisUrl))('shared reporting usage read', () => { transaction.mockClear() summedPeriods.length = 0 redisConfigMockFns.mockGetRedisClient.mockReturnValue(redis) + redisConfigMockFns.mockReleaseLock.mockImplementation(async (key: string, value: string) => { + if ((await redis.get(key)) !== value) return false + return (await redis.del(key)) === 1 + }) payer = { type: 'organization', id: generateId() } await connection`INSERT INTO usage_log (id, cost, billing_entity_type, billing_entity_id, created_at) VALUES (${generateId()}, 4.25, 'organization', ${payer.id}, '2026-03-01'), @@ -390,6 +394,29 @@ describe.runIf(Boolean(redisUrl))('shared reporting usage read', () => { expect(await exactSum(payer, period)).toBe(9) }) + it('refreshes a prefix discarded by a top-up once, instead of summing the window on every read', async () => { + const period = openReportingWindow() + await insertUsage(payer, 3, new Date(Date.now() - MINUTE_MS)) + const eventKey = await startCumulativeCharge( + payer, + 2, + new Date(period.start.getTime() + DAY_MS) + ) + await expect(readInNewProcess(payer, period)).resolves.toBe(5) + await prefixStored(payer, period) + await vi.waitFor(async () => expect(await redis.exists(prefixLeaseKey(payer, period))).toBe(0)) + + await recordCumulativeUsage(cumulativeCharge(payer, eventKey, 6)) + await vi.waitFor(async () => expect(await redis.exists(payerEpochKey(payer))).toBe(1)) + await redis.del(sharedKey(payer, period)) + await expect(readInNewProcess(payer, period)).resolves.toBe(9) + await redis.del(sharedKey(payer, period)) + await expect(readInNewProcess(payer, period)).resolves.toBe(9) + + expect(await exactSum(payer, period)).toBe(9) + expect(windowStartSums(period)).toHaveLength(2) + }) + it('leaves the prefix in place for a top-up to a row still in the tail, and reads it exactly', async () => { const period = openReportingWindow() await insertUsage(payer, 2, new Date(period.start.getTime() + DAY_MS)) diff --git a/apps/sim/lib/billing/core/reporting-usage-cache.ts b/apps/sim/lib/billing/core/reporting-usage-cache.ts index ba0ecf7c30a..60dc97d23fe 100644 --- a/apps/sim/lib/billing/core/reporting-usage-cache.ts +++ b/apps/sim/lib/billing/core/reporting-usage-cache.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' +import { generateShortId } from '@sim/utils/id' import { randomInt } from '@sim/utils/random' import type Redis from 'ioredis' import { LRUCache } from 'lru-cache' @@ -18,6 +19,7 @@ import { getBillingPeriodUsageCost, type UsageQueryPeriod, } from '@/lib/billing/core/usage-log' +import { releaseLock } from '@/lib/core/config/redis' import { withinDeadline } from '@/lib/core/utils/deadline' import type { DbClient } from '@/lib/db/types' @@ -85,7 +87,9 @@ const PREFIX_REFRESH_AFTER_MS = 3 * 60_000 * The refresh lease is held for as long as one ledger sum may run, so a second process starts * the same sum only once the first has certainly ended, and a holder that died frees it then. * Nothing tells a stranded lease from a live one, and nothing needs to: while it is held, every - * other reader serves the stored prefix or, with none, sums the whole window. + * other reader serves the stored prefix or, with none, sums the whole window. Its holder + * releases it once the new prefix is stored, so a prefix discarded by an epoch bump right after + * is refreshed by the next read rather than leaving every reader summing the whole window. */ const PREFIX_LEASE_MS = USAGE_LEDGER_STATEMENT_TIMEOUT_MS @@ -259,23 +263,24 @@ async function readStoredPrefix( } /** - * Whether this process took the refresh lease. It is never released: the stored prefix ends the - * need for it, and expiry frees it for the next refresh. A lease whose `SET` failed or timed out - * may still have landed, held by nobody, for {@link PREFIX_LEASE_MS}. Not `acquireLock`, which + * This process's token for the refresh lease, or `null` when it did not take it. A refresh that + * fails keeps the lease until expiry, as a backoff. A lease whose `SET` failed or timed out may + * still have landed, held by nobody, for {@link PREFIX_LEASE_MS}. Not `acquireLock`, which * issues on a client that is not ready and has no read deadline. */ -async function acquirePrefixLease(redis: Redis, key: string): Promise { +async function acquirePrefixLease(redis: Redis, key: string): Promise { + const token = generateShortId() try { const result = await withinDeadline( - () => redis.set(sharedPrefixLeaseKey(key), '1', 'PX', PREFIX_LEASE_MS, 'NX'), + () => redis.set(sharedPrefixLeaseKey(key), token, 'PX', PREFIX_LEASE_MS, 'NX'), Date.now() + SHARED_READ_TIMEOUT_MS ) - return result === 'OK' + return result === 'OK' ? token : null } catch (error) { logger.warn('Shared reporting usage prefix lease failed', { error: getErrorMessage(error), }) - return false + return null } } @@ -283,9 +288,10 @@ async function acquirePrefixLease(redis: Redis, key: string): Promise { * Fire-and-forget, and only on a ready client, like {@link writeSharedReportingUsageCost}. A * plain `SET` replaces the stored prefix: every prefix is a lower bound for its own cutoff, so * two refreshes racing can leave either one, and the tail always resumes from the cutoff stored - * with it. + * with it. Once it lands, the lease is released only while it still holds `lease`, so one that + * expired and was taken by another process is left to that process. */ -function writeStoredPrefix(key: string, prefix: LedgerPrefix): void { +function writeStoredPrefix(key: string, prefix: LedgerPrefix, lease: string): void { try { const redis = readyRedisClient() if (!redis) return @@ -296,6 +302,7 @@ function writeStoredPrefix(key: string, prefix: LedgerPrefix): void { 'PX', prefix.cutoff + PREFIX_CUTOFF_LAG_MS + PREFIX_RETAIN_MS - Date.now() ) + .then(() => releaseLock(sharedPrefixLeaseKey(key), lease)) .catch(warnSharedWriteFailed) } catch (error) { warnSharedWriteFailed(error) @@ -335,10 +342,11 @@ async function resolveLedgerPrefix( const { epochs, prefix: stored } = state if (stored && cutoff - stored.cutoff < PREFIX_REFRESH_AFTER_MS) return stored - if (await acquirePrefixLease(redis, key)) { + const lease = await acquirePrefixLease(redis, key) + if (lease) { const cost = getBillingPeriodUsageCost(entity, { ...period, end: new Date(cutoff) }).then( (prefixCost) => { - writeStoredPrefix(key, { ...epochs, cutoff, cost: prefixCost }) + writeStoredPrefix(key, { ...epochs, cutoff, cost: prefixCost }, lease) return prefixCost } ) diff --git a/apps/sim/lib/billing/core/usage-gate-cache.test.ts b/apps/sim/lib/billing/core/usage-gate-cache.test.ts index 79f35c65b3b..aeb4290683d 100644 --- a/apps/sim/lib/billing/core/usage-gate-cache.test.ts +++ b/apps/sim/lib/billing/core/usage-gate-cache.test.ts @@ -5,7 +5,7 @@ import { billingAttributionMockFns, } from '@sim/testing/mocks/billing-attribution.mock' import { sleep } from '@sim/utils/helpers' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/billing/core/attribution', () => billingAttributionMock) @@ -14,6 +14,7 @@ import { checkExecutionUsageLimits, checkIngestionUsageLimits, checkSearchUsageLimits, + NEAR_LIMIT_USAGE_GATE_TTL_MS, resetUsageGateCache, USAGE_GATE_SETTLE_TIMEOUT_MS, USAGE_GATE_TTL_MS, @@ -21,6 +22,30 @@ import { const mockCheck = billingAttributionMockFns.mockCheckAttributedUsageLimits +/** Fakes `setTimeout` for {@link advanceClockPast}; install it before the cache is first read. */ +function useFakeTimeouts(): void { + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) +} + +/** + * Fires the cache's pending clock debounce before restoring real timers; dropped unfired, it would + * pin the module-level cache to this test's spied clock for every later test. + */ +function restoreRealTimers(): void { + vi.runOnlyPendingTimers() + vi.useRealTimers() +} + +/** + * `lru-cache` captures `performance` at load, so fake timers cannot move its clock: spy the clock + * forward, then fire the faked 1 ms timer behind which it debounces `performance.now()`. + */ +async function advanceClockPast(ms: number): Promise { + const start = performance.now() + vi.spyOn(performance, 'now').mockReturnValue(start + ms + 1) + await vi.advanceTimersByTimeAsync(1) +} + const ATTRIBUTION: BillingAttributionSnapshot = { actorUserId: 'member-1', workspaceId: null, @@ -74,17 +99,19 @@ describe('checkIngestionUsageLimits', () => { }) it('keeps a refusal for the same bounded window, then reads fresh', async () => { - mockCheck.mockResolvedValueOnce({ isExceeded: true, scope: 'payer', message: 'over' }) - expect((await checkIngestionUsageLimits(ATTRIBUTION)).isExceeded).toBe(true) - expect((await checkIngestionUsageLimits(ATTRIBUTION)).isExceeded).toBe(true) - expect(mockCheck).toHaveBeenCalledTimes(1) + useFakeTimeouts() + try { + mockCheck.mockResolvedValueOnce({ isExceeded: true, scope: 'payer', message: 'over' }) + expect((await checkIngestionUsageLimits(ATTRIBUTION)).isExceeded).toBe(true) + expect((await checkIngestionUsageLimits(ATTRIBUTION)).isExceeded).toBe(true) + expect(mockCheck).toHaveBeenCalledTimes(1) - /** `lru-cache` reads `performance.now()` and debounces it behind a real 1 ms timer. */ - const start = performance.now() - vi.spyOn(performance, 'now').mockReturnValue(start + USAGE_GATE_TTL_MS + 1) - await sleep(5) - expect((await checkIngestionUsageLimits(ATTRIBUTION)).isExceeded).toBe(false) - expect(mockCheck).toHaveBeenCalledTimes(2) + await advanceClockPast(USAGE_GATE_TTL_MS) + expect((await checkIngestionUsageLimits(ATTRIBUTION)).isExceeded).toBe(false) + expect(mockCheck).toHaveBeenCalledTimes(2) + } finally { + restoreRealTimers() + } }) it('separates answers by actor, period, period source, payer and plan', async () => { @@ -212,3 +239,72 @@ describe('checkExecutionUsageLimits', () => { } }) }) + +describe('admission lifetime by headroom', () => { + beforeEach(() => { + useFakeTimeouts() + resetUsageGateCache() + mockCheck.mockReset() + }) + + afterEach(() => { + restoreRealTimers() + }) + + it('re-reads a free-plan admission after the near-limit window, so spend past the limit is refused', async () => { + mockCheck + .mockResolvedValueOnce({ isExceeded: false, payerUsage: { currentUsage: 1, limit: 5 } }) + .mockResolvedValueOnce({ + isExceeded: true, + scope: 'payer', + message: 'over', + payerUsage: { currentUsage: 5.4, limit: 5 }, + }) + expect((await checkExecutionUsageLimits(ATTRIBUTION)).isExceeded).toBe(false) + expect((await checkExecutionUsageLimits(ATTRIBUTION)).isExceeded).toBe(false) + expect(mockCheck).toHaveBeenCalledTimes(1) + + await advanceClockPast(NEAR_LIMIT_USAGE_GATE_TTL_MS) + expect((await checkExecutionUsageLimits(ATTRIBUTION)).isExceeded).toBe(true) + expect(mockCheck).toHaveBeenCalledTimes(2) + }) + + it('re-reads a large pool in the last fifth of its limit', async () => { + mockCheck.mockResolvedValue({ + isExceeded: false, + payerUsage: { currentUsage: 900, limit: 1000 }, + }) + await checkExecutionUsageLimits(ATTRIBUTION) + await advanceClockPast(NEAR_LIMIT_USAGE_GATE_TTL_MS) + await checkExecutionUsageLimits(ATTRIBUTION) + expect(mockCheck).toHaveBeenCalledTimes(2) + }) + + it('re-reads when the acting member is near their cap though the pool has room', async () => { + mockCheck.mockResolvedValue({ + isExceeded: false, + payerUsage: { currentUsage: 100, limit: 1000 }, + memberUsage: { currentUsage: 48, limit: 50 }, + }) + await checkSearchUsageLimits(ATTRIBUTION) + await advanceClockPast(NEAR_LIMIT_USAGE_GATE_TTL_MS) + await checkSearchUsageLimits(ATTRIBUTION) + expect(mockCheck).toHaveBeenCalledTimes(2) + }) + + it('holds an admission with ample headroom for the full TTL', async () => { + mockCheck.mockResolvedValue({ + isExceeded: false, + payerUsage: { currentUsage: 100, limit: 1000 }, + memberUsage: { currentUsage: 10, limit: null }, + }) + await checkExecutionUsageLimits(ATTRIBUTION) + await advanceClockPast(NEAR_LIMIT_USAGE_GATE_TTL_MS) + await checkExecutionUsageLimits(ATTRIBUTION) + expect(mockCheck).toHaveBeenCalledTimes(1) + + await advanceClockPast(USAGE_GATE_TTL_MS) + await checkExecutionUsageLimits(ATTRIBUTION) + expect(mockCheck).toHaveBeenCalledTimes(2) + }) +}) diff --git a/apps/sim/lib/billing/core/usage-gate-cache.ts b/apps/sim/lib/billing/core/usage-gate-cache.ts index 230cf09e7d7..30677b92921 100644 --- a/apps/sim/lib/billing/core/usage-gate-cache.ts +++ b/apps/sim/lib/billing/core/usage-gate-cache.ts @@ -13,14 +13,50 @@ import { coalesceLocally } from '@/lib/concurrency/singleflight' * The gate sums the payer's usage ledger for the billing period, which grows * with the payer's activity, so a large organization scans its whole period on * every uncached call. Bulk ingestion re-checks per document and knowledge - * search checks per query. Staleness is bounded by this TTL and fails in the - * harmless direction: a payer who crosses their limit keeps going for at most - * this long, which charges nobody wrongly. Five minutes: the sum is a few - * hundred milliseconds for a busy payer, and a minute made every search after - * a pause pay it. + * search checks per query. A payer who crosses their limit keeps going for at + * most this long, so only an admission with ample headroom is held this long + * (see {@link usageAdmissionTtlMs}). Five minutes: the sum is a few hundred + * milliseconds for a busy payer, and a minute made every search after a pause + * pay it. */ export const USAGE_GATE_TTL_MS = 5 * 60 * 1000 +/** + * How long an admission near its limit stays usable. Every spend landing + * inside this window is spend past the limit nobody bills (a free plan) or + * past a cap the payer set, so it is short; it stays above zero so a burst of + * executions or model steps from one payer shares a read instead of summing + * the ledger on each. Staleness is this plus any lag in the usage read itself. + */ +export const NEAR_LIMIT_USAGE_GATE_TTL_MS = 10 * 1000 + +/** + * An admission is near its limit when the remaining headroom is under the + * larger of this floor and {@link NEAR_LIMIT_HEADROOM_SHARE} of the limit. The + * floor puts every free-plan payer permanently near its limit; the share keeps + * a large pool from caching its last stretch. + */ +const NEAR_LIMIT_HEADROOM_FLOOR_USD = 25 +const NEAR_LIMIT_HEADROOM_SHARE = 0.2 + +type UsageStanding = NonNullable + +function isNearLimit(usage: UsageStanding | undefined): boolean { + if (!usage || usage.limit === null) return false + const headroom = usage.limit - usage.currentUsage + return headroom < Math.max(NEAR_LIMIT_HEADROOM_FLOOR_USD, usage.limit * NEAR_LIMIT_HEADROOM_SHARE) +} + +/** + * How long an admission may be served from cache: the full gate TTL while + * every standing (the payer's, the acting member's) has ample headroom, + * otherwise {@link NEAR_LIMIT_USAGE_GATE_TTL_MS}. An admission that carries no + * figures (billing off) has no limit to approach. + */ +export function usageAdmissionTtlMs(...standings: Array): number { + return standings.some(isNearLimit) ? NEAR_LIMIT_USAGE_GATE_TTL_MS : USAGE_GATE_TTL_MS +} + /** * How long a coalesced usage read may take before its callers give up on it. The read's cost is * its ledger aggregates, each of which the database ends at @@ -78,7 +114,8 @@ function gateKey(attribution: BillingAttributionSnapshot): string { * `coalesceLocally` collapses concurrent misses onto one ledger read and bounds * a hung read at {@link USAGE_GATE_SETTLE_TIMEOUT_MS}. The write stays on the * value this caller received, so a producer that timed out and later resolved - * cannot overwrite a fresher answer. + * cannot overwrite a fresher answer. An admission is stored for + * {@link usageAdmissionTtlMs}, a refusal for the full TTL. * * There is deliberately no invalidator: usage and limit changes land in other * processes (execution workers, Stripe webhooks), so the TTL is the real bound. @@ -96,7 +133,13 @@ async function checkUsageLimitsThroughCache( () => checkAttributedUsageLimits(attribution), USAGE_GATE_SETTLE_TIMEOUT_MS ) - if (cacheRefusals || !result.isExceeded) gateCache.set(key, result) + if (cacheRefusals || !result.isExceeded) { + gateCache.set(key, result, { + ttl: result.isExceeded + ? USAGE_GATE_TTL_MS + : usageAdmissionTtlMs(result.payerUsage, result.memberUsage), + }) + } return result } diff --git a/apps/sim/lib/billing/core/usage-log.integration.ts b/apps/sim/lib/billing/core/usage-log.integration.ts index 9996d2dd698..b6866e49665 100644 --- a/apps/sim/lib/billing/core/usage-log.integration.ts +++ b/apps/sim/lib/billing/core/usage-log.integration.ts @@ -28,6 +28,7 @@ vi.mock('@/lib/billing/subscriptions/utils', async (importOriginal) => ({ import { CumulativeUsageContextMismatchError, + CumulativeUsageInFlightError, CumulativeUsagePeriodClosedError, getBillingPeriodUsageCost, getBillingPeriodUsageCostByUser, @@ -92,6 +93,18 @@ function usage(cost: number, eventKey = 'update-cost:shared-request'): RecordCum } } +/** Resends a callback refused as in flight, the way the worker's replay does, until it is recorded. */ +async function untilRecorded(params: RecordCumulativeUsageParams) { + for (let attempt = 0; ; attempt++) { + try { + return await recordCumulativeUsage(params) + } catch (error) { + if (!(error instanceof CumulativeUsageInFlightError) || attempt >= 200) throw error + await sleep(50) + } + } +} + async function ledgerRows() { return connection<{ event_key: string; cost: string }[]>` select event_key, cost from usage_log order by event_key @@ -199,13 +212,15 @@ describe('Cumulative billing with PostgreSQL', () => { } ) - it('reproduces a retry timing out behind an idle holder when only lock waits are bounded', async () => { + it('refuses a retry behind a holder of the same request at once, without waiting on its lock', async () => { const pause = pauseNextTransaction(true) const holder = recordCumulativeUsage(usage(0.4)) try { await pause.reached.promise + const startedAt = performance.now() const failure = await recordCumulativeUsage(usage(0.8)).catch((error: unknown) => error) - expect(getPostgresErrorCode(failure)).toBe('55P03') + expect(failure).toBeInstanceOf(CumulativeUsageInFlightError) + expect(performance.now() - startedAt).toBeLessThan(1_000) expect(await ledgerRows()).toEqual([]) } finally { pause.release.resolve() @@ -223,8 +238,8 @@ describe('Cumulative billing with PostgreSQL', () => { try { await pause.reached.promise const failure = await recordCumulativeUsage(usage(0.8)).catch((error: unknown) => error) - expect(getPostgresErrorCode(failure)).toBe('55P03') - const recovered = await recordCumulativeUsage(usage(0.8)) + expect(failure).toBeInstanceOf(CumulativeUsageInFlightError) + const recovered = await untilRecorded(usage(0.8)) expect(recovered.billed).toBe(true) expect(recovered.delta).toBeCloseTo(0.8 - initial, 9) expect(recovered.total).toBe(0.8) @@ -249,7 +264,7 @@ describe('Cumulative billing with PostgreSQL', () => { it('converges concurrent out-of-order callbacks and independent events to their exact totals', async () => { const costs = [0.4, 0.1, 0.8, 0.3, 0.8, 0.6] - const results = await Promise.all(costs.map((cost) => recordCumulativeUsage(usage(cost)))) + const results = await Promise.all(costs.map((cost) => untilRecorded(usage(cost)))) expect(results.reduce((total, result) => total + result.delta, 0)).toBeCloseTo(0.8, 9) await Promise.all( Array.from({ length: 32 }, (_, index) => diff --git a/apps/sim/lib/billing/core/usage-log.test.ts b/apps/sim/lib/billing/core/usage-log.test.ts index 9a1d2d73523..c40202faa5f 100644 --- a/apps/sim/lib/billing/core/usage-log.test.ts +++ b/apps/sim/lib/billing/core/usage-log.test.ts @@ -5,6 +5,7 @@ import { billingSubscriptionUtilsMock, billingSubscriptionUtilsMockFns, } from '@sim/testing/mocks/billing-subscription-utils.mock' +import { getMockLogger } from '@sim/testing/mocks/logger.mock' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { @@ -31,6 +32,7 @@ import { USAGE_LEDGER_STATEMENT_TIMEOUT_MS } from '@/lib/billing/constants' import { CUMULATIVE_COST_EPSILON, CumulativeUsageContextMismatchError, + CumulativeUsageInFlightError, getBillingPeriodUsageCost, getBillingPeriodUsageCostByUser, getBillingPeriodUsageCostWithSourceSubset, @@ -245,7 +247,8 @@ describe('recordCumulativeUsage', () => { mockIsOrgScopedSubscription.mockReturnValue(true) }) - const setupTx = (existingRow: Partial | null) => { + /** `locked`: another attempt of the request holds its ledger lock. */ + const setupTx = (existingRow: Partial | null, locked = false) => { const resolvedExistingRow = existingRow ? { ...defaultExistingRow, ...existingRow } : null const limit = vi.fn().mockResolvedValue(resolvedExistingRow ? [resolvedExistingRow] : []) const where = vi.fn().mockReturnValue({ limit }) @@ -255,7 +258,8 @@ describe('recordCumulativeUsage', () => { const updateSet = vi.fn().mockReturnValue({ where: updateWhere }) mockUpdate.mockReturnValue({ set: updateSet }) const tx = { - execute: vi.fn().mockResolvedValue(undefined), + // The configure statement's result is unused; the try-lock reads `acquired`. + execute: vi.fn().mockResolvedValue([{ acquired: !locked }]), select, update: mockUpdate, insert: mockInsert, // recordUsage(tx) reuses the shared insert chain @@ -290,6 +294,27 @@ describe('recordCumulativeUsage', () => { }) }) + it('refuses at once, writing nothing, while another attempt of the request holds its lock', async () => { + const { select } = setupTx(null, true) + await expect( + recordCumulativeUsage({ + userId: 'user-1', + source: 'workspace-chat', + model: 'claude-opus-4.8', + cost: 0.4662453, + eventKey: 'update-cost:msg-1-billing', + }) + ).rejects.toBeInstanceOf(CumulativeUsageInFlightError) + expect(select).not.toHaveBeenCalled() + expect(mockInsert).not.toHaveBeenCalled() + expect(mockUpdate).not.toHaveBeenCalled() + // The route logs the refusal once; it is not a transaction that failed to complete. + expect(getMockLogger('UsageLog').warn).not.toHaveBeenCalledWith( + 'Cumulative usage transaction did not complete promptly', + expect.anything() + ) + }) + it('tops up to the higher cumulative and bills only the delta', async () => { const { updateSet } = setupTx({ id: 'row-1', cost: '0.3474447' }) const result = await recordCumulativeUsage({ diff --git a/apps/sim/lib/billing/core/usage-log.ts b/apps/sim/lib/billing/core/usage-log.ts index 78bfb51ac06..2c29a25e4f1 100644 --- a/apps/sim/lib/billing/core/usage-log.ts +++ b/apps/sim/lib/billing/core/usage-log.ts @@ -30,7 +30,7 @@ import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' import type { InternalUsageLogSource } from '@/lib/billing/usage-sources' import { asOrchestrationError, OrchestrationError } from '@/lib/core/orchestration/types' import { HttpError } from '@/lib/core/utils/http-error' -import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks' +import { tryAcquireAdvisoryXactLock } from '@/lib/db/advisory-locks' import type { DbClient, DbOrTx } from '@/lib/db/types' const logger = createLogger('UsageLog') @@ -700,6 +700,18 @@ export class CumulativeUsagePeriodClosedError extends Error { } } +/** + * Another attempt of the same request holds its ledger lock, so this one cannot know whether that + * attempt's charge will land. Waiting would only hold a connection for an answer the caller gets by + * retrying later: the monotonic top-up applies whichever cumulative arrives once. + */ +export class CumulativeUsageInFlightError extends Error { + constructor(readonly eventKey: string) { + super(`Cumulative usage event "${eventKey}" is already being recorded`) + this.name = 'CumulativeUsageInFlightError' + } +} + interface CumulativeUsageLedgerBinding { userId: string workspaceId: string | null @@ -755,11 +767,13 @@ function assertCumulativeUsageLedgerBinding( } /** - * PostgreSQL 17+ bounds the entire transaction below the callback's five-second - * deadline. Older supported servers instead bound each idle interval between - * statements, alongside the per-statement budget. Both policies release an idle - * lock holder without waiting for its application process to resume; only the - * newer policy also limits total elapsed transaction time. + * PostgreSQL 17+ bounds the transaction's statements and the idle time between + * them; older supported servers bound each idle interval instead, alongside the + * per-statement budget. Both release an idle lock holder without waiting for its + * application process to resume. Neither bounds COMMIT: a commit waiting for a + * synchronous standby keeps its locks, including the request's ledger lock, until + * the standby confirms, however long that takes. `lock_timeout` bounds the row + * locks taken under the ledger lock; the ledger lock itself is never waited for. */ const CUMULATIVE_FLUSH_TRANSACTION_TIMEOUT_MS = 4_000 const CUMULATIVE_FLUSH_STATEMENT_TIMEOUT_MS = 3_500 @@ -778,10 +792,12 @@ type CumulativeUsageStage = 'pool' | 'configure' | 'lock' | 'read' | 'write' | ' * (no two flushes can both believe they are first and clobber each other). * An existing row must match the incoming actor, workspace, payer, and billing * period before either a duplicate no-op or a top-up is accepted. - * The billing context is resolved BEFORE the transaction and the lock wait is - * bounded by `lock_timeout`. A server-enforced transaction deadline, or idle - * transaction deadline on older PostgreSQL, releases a stalled holder. The - * critical section uses one SELECT plus one INSERT/UPDATE on a single connection. + * The billing context is resolved BEFORE the transaction. The lock is taken + * without waiting: a held lock means another attempt of the same request is + * still being recorded (possibly committing behind a synchronous standby), and + * this attempt throws {@link CumulativeUsageInFlightError} instead of holding a + * connection while it waits. The critical section uses one SELECT plus one + * INSERT/UPDATE on a single connection. * * Because every leg flushes its cumulative and this converges to the max, * there is no under-billing if the request recovers after a partial flush, no @@ -816,6 +832,7 @@ export async function recordCumulativeUsage( const stageDurationsMs: Partial> = {} let succeeded = false let pgCode: string | undefined + let inFlight = false let toppedUpRowCreatedAt: Date | undefined const enterStage = (nextStage: CumulativeUsageStage) => { const now = Date.now() @@ -841,7 +858,9 @@ export async function recordCumulativeUsage( set_config('lock_timeout', ${`${CUMULATIVE_FLUSH_LOCK_TIMEOUT_MS}ms`}, true) `) enterStage('lock') - await acquireAdvisoryXactLock(tx, 'usage_log_event', eventKey) + if (!(await tryAcquireAdvisoryXactLock(tx, 'usage_log_event', eventKey))) { + throw new CumulativeUsageInFlightError(eventKey) + } enterStage('read') const rows = await tx @@ -997,12 +1016,14 @@ export async function recordCumulativeUsage( return result } catch (error) { pgCode = getPostgresErrorCode(error) + // Another attempt holding the lock is an answer, not a slow transaction; the route logs it. + if (error instanceof CumulativeUsageInFlightError) inFlight = true throw error } finally { const now = Date.now() stageDurationsMs[stage] = now - stageStartedAt const durationMs = now - startedAt - if (!succeeded || durationMs >= CUMULATIVE_FLUSH_SLOW_MS) { + if (!inFlight && (!succeeded || durationMs >= CUMULATIVE_FLUSH_SLOW_MS)) { logger.warn('Cumulative usage transaction did not complete promptly', { eventKey, succeeded, diff --git a/apps/sim/lib/billing/organizations/membership.ts b/apps/sim/lib/billing/organizations/membership.ts index aa54a173b42..6af43569466 100644 --- a/apps/sim/lib/billing/organizations/membership.ts +++ b/apps/sim/lib/billing/organizations/membership.ts @@ -59,6 +59,7 @@ import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks' import { isRetryableTransactionError } from '@/lib/db/transaction' import type { DbOrTx, DbTransaction } from '@/lib/db/types' import { acquireInvitationMutationLocks } from '@/lib/invitations/locks' +import { type StoppedRun, stopLiveRunsTx } from '@/lib/mothership/async-runs/repository' import { requireMemberManagementAuthority } from '@/lib/organizations/members/authority' import { revokePersonalApiKeysTx, @@ -543,6 +544,11 @@ export interface RemoveMemberResult { workspaceAccessRevoked: number pendingInvitationsCancelled: number } + /** + * Chat runs the change stopped in its transaction. The caller delivers their Stop with + * `deliverRunStops`: this module sits in page graphs that must not load that delivery. + */ + stoppedRuns: StoppedRun[] } export interface RemoveExternalWorkspaceAccessResult { @@ -552,6 +558,11 @@ export interface RemoveExternalWorkspaceAccessResult { permissionGroupsRevoked: number credentialMembershipsRevoked: number pendingInvitationsCancelled: number + /** + * Chat runs the change stopped in its transaction. The caller delivers their Stop with + * `deliverRunStops`: this module sits in page graphs that must not load that delivery. + */ + stoppedRuns: StoppedRun[] } export type MembershipAdditionFailureCode = @@ -1095,6 +1106,11 @@ export interface TransferOrganizationMemberResult { credentialMembershipsRevoked: number pendingInvitationsCancelled: number usageCaptured: number + /** + * Chat runs the change stopped in its transaction. The caller delivers their Stop with + * `deliverRunStops`: this module sits in page graphs that must not load that delivery. + */ + stoppedRuns: StoppedRun[] } /** @@ -1111,6 +1127,7 @@ export async function transferUserBetweenOrganizations( credentialMembershipsRevoked: 0, pendingInvitationsCancelled: 0, usageCaptured: 0, + stoppedRuns: [], } if (params.sourceOrganizationId === params.destinationOrganizationId) { return { @@ -1242,6 +1259,14 @@ export async function transferUserBetweenOrganizations( ) await removeWorkspaceSkillMembershipsTx(tx, workspaceIds, params.userId) } + /** The source organization's runs this transfer ends. */ + const stoppedRuns = [ + ...(await stopLiveRunsTx(tx, { + userId: params.userId, + organizationId: params.sourceOrganizationId, + })), + ...(await stopLiveRunsTx(tx, { userId: params.userId, workspaceIds })), + ] const added = await ensureUserInOrganizationTx(tx, { userId: params.userId, @@ -1271,6 +1296,7 @@ export async function transferUserBetweenOrganizations( // Nothing to capture: the member's ledger rows stay stamped to the // source org's period and are billed at its cycle close. usageCaptured: 0, + stoppedRuns, } } ) @@ -1334,11 +1360,16 @@ export async function removeUserFromOrganization( .limit(1) if (!existingMember) { - return { success: false, error: 'Member not found', billingActions } + return { success: false, error: 'Member not found', billingActions, stoppedRuns: [] } } if (existingMember.role === 'owner') { - return { success: false, error: 'Cannot remove organization owner', billingActions } + return { + success: false, + error: 'Cannot remove organization owner', + billingActions, + stoppedRuns: [], + } } const result = await withInvitationSafeOrganizationAccessMutation( @@ -1431,6 +1462,11 @@ export async function removeUserFromOrganization( }) if (revokePersonalApiKeys) await revokePersonalApiKeysTx(tx, { userId }) await endDirectoryMembershipTx(tx, { userId, organizationId }) + /** The member's organization Assistant runs and their runs in its workspaces end with access. */ + const stoppedRuns = [ + ...(await stopLiveRunsTx(tx, { userId, organizationId })), + ...(await stopLiveRunsTx(tx, { userId, workspaceIds })), + ] if (workspaceIds.length === 0) { return { @@ -1441,6 +1477,7 @@ export async function removeUserFromOrganization( usageCaptured: 0, credentialMembershipsRevoked: 0, pendingInvitationsCancelled: cancelledInvitations.length, + stoppedRuns, } } @@ -1478,6 +1515,7 @@ export async function removeUserFromOrganization( usageCaptured: 0, credentialMembershipsRevoked, pendingInvitationsCancelled: cancelledInvitations.length, + stoppedRuns, } } ) @@ -1488,7 +1526,7 @@ export async function removeUserFromOrganization( userId, memberId, }) - return { success: true, removed: false, billingActions } + return { success: true, removed: false, billingActions, stoppedRuns: [] } } billingActions.usageCaptured = result.usageCaptured @@ -1551,10 +1589,10 @@ export async function removeUserFromOrganization( } } - return { success: true, removed: true, billingActions } + return { success: true, removed: true, billingActions, stoppedRuns: result.stoppedRuns } } catch (error) { if (error instanceof WorkspaceBillingAccountRemovalError) { - return { success: false, error: error.message, billingActions } + return { success: false, error: error.message, billingActions, stoppedRuns: [] } } if (onError === 'throw') throw error @@ -1564,7 +1602,12 @@ export async function removeUserFromOrganization( memberId, error, }) - return { success: false, error: 'Failed to remove user from organization', billingActions } + return { + success: false, + error: 'Failed to remove user from organization', + billingActions, + stoppedRuns: [], + } } } @@ -1578,6 +1621,13 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { actorUserId?: string }): Promise { const { userId, organizationId } = params + const nothingRemoved = { + workspaceAccessRevoked: 0, + permissionGroupsRevoked: 0, + credentialMembershipsRevoked: 0, + pendingInvitationsCancelled: 0, + stoppedRuns: [], + } try { const [existingMember] = await db @@ -1590,10 +1640,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { return { success: false, error: 'User is an organization member', - workspaceAccessRevoked: 0, - permissionGroupsRevoked: 0, - credentialMembershipsRevoked: 0, - pendingInvitationsCancelled: 0, + ...nothingRemoved, } } @@ -1602,6 +1649,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { permissionGroupsRevoked, credentialMembershipsRevoked, pendingInvitationsCancelled, + stoppedRuns, } = await withInvitationSafeOrganizationAccessMutation( { userId, organizationId, scope: 'external' }, async (tx, { workspaceIds, invitationIds }) => { @@ -1658,6 +1706,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { permissionGroupsRevoked: deletedPermissionGroups.length, credentialMembershipsRevoked: 0, pendingInvitationsCancelled: cancelledInvitations.length, + stoppedRuns: [], } } @@ -1694,6 +1743,8 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { permissionGroupsRevoked: deletedPermissionGroups.length, credentialMembershipsRevoked, pendingInvitationsCancelled: cancelledInvitations.length, + /** The member's runs in the workspaces they lose. */ + stoppedRuns: await stopLiveRunsTx(tx, { userId, workspaceIds }), } } ) @@ -1711,6 +1762,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { permissionGroupsRevoked, credentialMembershipsRevoked, pendingInvitationsCancelled, + stoppedRuns, } } @@ -1729,6 +1781,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { permissionGroupsRevoked, credentialMembershipsRevoked, pendingInvitationsCancelled, + stoppedRuns, } } catch (error) { if (error instanceof OrchestrationError || isRetryableTransactionError(error)) throw error @@ -1736,10 +1789,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { return { success: false, error: error.message, - workspaceAccessRevoked: 0, - permissionGroupsRevoked: 0, - credentialMembershipsRevoked: 0, - pendingInvitationsCancelled: 0, + ...nothingRemoved, } } @@ -1751,10 +1801,7 @@ export async function removeExternalUserFromOrganizationWorkspaces(params: { return { success: false, error: 'Failed to remove external workspace member', - workspaceAccessRevoked: 0, - permissionGroupsRevoked: 0, - credentialMembershipsRevoked: 0, - pendingInvitationsCancelled: 0, + ...nothingRemoved, } } } diff --git a/apps/sim/lib/changelog/mdx.tsx b/apps/sim/lib/changelog/mdx.tsx index 770642d458d..620c66a2e1a 100644 --- a/apps/sim/lib/changelog/mdx.tsx +++ b/apps/sim/lib/changelog/mdx.tsx @@ -1,13 +1,14 @@ import type { ComponentPropsWithoutRef } from 'react' import { cn } from '@sim/emcn' import type { MDXRemoteProps } from 'next-mdx-remote/rsc' -import { ChangelogImage, ChangelogVideo } from '@/app/(landing)/changelog/components' +import { ChangelogImage, ChangelogLink, ChangelogVideo } from '@/app/(landing)/changelog/components' import { HOME_TYPE } from '@/app/(landing)/components/landing-layout' import { PROSE_TYPE } from '@/app/(landing)/components/prose-page/constants' export const changelogComponents = { ChangelogVideo, ChangelogImage, + a: ChangelogLink, h3: ({ className, ...props }: ComponentPropsWithoutRef<'h3'>) => (

), diff --git a/apps/sim/lib/collab-doc/state.ts b/apps/sim/lib/collab-doc/state.ts index 9af5c13e919..a0f7bbba020 100644 --- a/apps/sim/lib/collab-doc/state.ts +++ b/apps/sim/lib/collab-doc/state.ts @@ -7,6 +7,13 @@ import type { DbOrTx, DbTransaction } from '@/lib/db/types' /** Matches the decoded size of the persist endpoint's 16 MiB base64 snapshot limit. */ export const MAX_COLLAB_DOC_STATE_BYTES = 12 * 1024 * 1024 +/** + * Body cap of the relay's persist routes: a base64-encoded maximum snapshot plus 1 MiB for its JSON + * envelope (17 MiB). Both routes bypass the proxy, whose 10 MB buffer would truncate it. + */ +export const MAX_COLLAB_DOC_PERSIST_BODY_BYTES = + Math.ceil(MAX_COLLAB_DOC_STATE_BYTES / 3) * 4 + 1024 * 1024 + /** Reject oversized snapshots before copying, decoding, or writing them. */ export function assertCollabDocStateSize(docState: Uint8Array): void { if (docState.byteLength > MAX_COLLAB_DOC_STATE_BYTES) { diff --git a/apps/sim/lib/compare/data/competitors/dust.ts b/apps/sim/lib/compare/data/competitors/dust.ts index 2e6aebed4cb..f2ac7418546 100644 --- a/apps/sim/lib/compare/data/competitors/dust.ts +++ b/apps/sim/lib/compare/data/competitors/dust.ts @@ -44,6 +44,24 @@ export const dustProfile: CompetitorProfile = { 'Both products provide documentation and learning resources. Sim emphasizes its open-source community and an Enterprise dedicated-support option, while Dust also documents community forums and enterprise onboarding.', ], }, + faqs: [ + { + question: + 'We use Dust for company knowledge but need real workflow automation too. What should we look at?', + answer: + "Sim is worth evaluating when Dust's knowledge agents need to run inside multi-step automations. Dust builds agents from instructions, tools, and connected data sources, and offers no loop container or branch-and-join execution model. Sim adds a visual workflow canvas with loops, parallel branches, error-handling paths, and human approval steps, started by webhooks, schedules, API calls, or events from connected apps.", + }, + { + question: 'Sim vs Dust for building company knowledge agents: which is better?', + answer: + "It depends on where the agent needs to live. Dust connects agents to sources such as Slack, Notion, Google Drive, Confluence, and GitHub and serves them in its web chat, Slack, and Microsoft Teams, with access scoped by Spaces. Sim's knowledge base combines vector and full-text search over 15 file types with connectors that keep sources such as Google Drive, Confluence, and Slack in sync, and a knowledge agent can be published as a chat interface, a REST API, or an MCP server. Dust suits a managed assistant used mainly in chat; Sim suits a knowledge agent that also has to feed workflows or other systems.", + }, + { + question: 'How does Sim compare to Dust for shared team AI assistants?', + answer: + 'Both are built for teams, but they share different things. Dust shares agents and conversations: people and agents work in a common workspace, access is set by roles plus Spaces, and Pro seats cost $30 or €30 per month ($24 or €24 per month billed annually) with 8,000 credits each. Sim shares the build: several people can edit the same workflow canvas live, workspaces carry admin, write, and read permissions, and the Pro plan is $25 per user per month.', + }, + ], standoutFeatures: [ { title: @@ -195,29 +213,34 @@ export const dustProfile: CompetitorProfile = { value: "No: the core repository is MIT-licensed and public on GitHub, but self-hosting isn't an officially supported deployment path; Dust is sold and operated only as hosted SaaS", detail: - 'dust-tt/dust is publicly available and MIT-licensed, but Dust the company documents only its hosted product (with US/EU region choice), not a supported self-managed installation.', + 'dust-tt/dust is publicly available and MIT-licensed, but Dust documents only its hosted product: US or EU data residency on Business, plus single-tenant deployment on Enterprise. It does not document a supported self-managed installation.', shortValue: 'No, MIT code exists but only SaaS is supported', confidence: 'estimated', sources: [ { url: 'https://github.com/dust-tt/dust/blob/main/LICENSE', label: 'dust-tt/dust LICENSE (GitHub)', - asOf: '2026-07-02', + asOf: '2026-10-15', + }, + { + url: 'https://dust.tt/home/pricing', + label: 'Dust Pricing', + asOf: '2026-10-15', }, ], }, deploymentOptions: { value: - 'Multi-tenant hosted cloud by default; an EU data-hosting option for databases, files, and vectors is available, documented as exclusive to Enterprise customers rather than a self-serve region choice on every tier', + 'Hosted cloud only, with US or EU data residency on the Business plan and single-tenant deployment on Enterprise', detail: - "Dust's changelog states the EU data hosting option is 'available exclusively for enterprise customers' and covers 'storage of databases, files, and vectors' (calls to third-party LLM provider APIs remain outside this hosting boundary). No Dust source documents a selectable US-region toggle or a named single-tenant deployment tier.", - shortValue: 'Multi-tenant cloud by default; EU data hosting is Enterprise-exclusive', - confidence: 'estimated', + 'Dust\'s pricing page lists "US & EU data residency" on Business and "US & EU data residency & single-tenant deployment" on Enterprise. Calls to third-party LLM provider APIs remain outside the hosting boundary.', + shortValue: 'Hosted cloud; US/EU residency, single-tenant on Enterprise', + confidence: 'verified', sources: [ { - url: 'https://docs.dust.tt/docs/changelog#-eu-data-hosting-option-available', - label: 'EU data hosting option available | Dust changelog', - asOf: '2026-07-08', + url: 'https://dust.tt/home/pricing', + label: 'Dust Pricing', + asOf: '2026-10-15', }, ], }, @@ -247,7 +270,7 @@ export const dustProfile: CompetitorProfile = { { url: 'https://github.com/dust-tt/dust/blob/main/LICENSE', label: 'dust-tt/dust LICENSE (GitHub)', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -752,61 +775,74 @@ export const dustProfile: CompetitorProfile = { }, pricing: { pricingModel: { - value: 'Per-seat subscription with a monthly AI-usage credit allocation per seat', + value: + 'Per-seat subscription (Free, Pro, and Max seat types on the Business plan, for teams up to 100 people) with a monthly AI-usage credit allocation per paid seat; Enterprise is sales-led', detail: - 'Business plan seats (Pro, Max) each include a monthly credit allocation that resets every billing period; credit consumption depends on the model used, task complexity, and any tools the agent invokes (search, retrieval, code execution, connected-app actions).', + 'Pro and Max seats each include a monthly credit allocation that resets every billing period. Credits are charged per message based on the model used and the actions performed.', shortValue: 'Per-seat pricing with monthly credit allocations', confidence: 'verified', sources: [ { url: 'https://dust.tt/home/pricing', label: 'Dust Pricing', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, entryPaidPlan: { - value: 'Business Pro: $30/month per seat ($24/month billed yearly), 8,000 credits/month', + value: + 'Business Pro seat: 30/month per seat (24/month billed yearly), 8,000 credits per seat per month; shown in dollars in the US and euros elsewhere, excluding VAT', detail: - "The Business plan's Pro tier is the entry paid tier above the free plan; a higher Max tier ($150/month, or $120/month billed yearly) includes 40,000 credits/month.", - shortValue: '$30/month per seat (or $24/month annual), 8,000 credits', + 'Pro is the entry paid seat above the Free seat. The higher Max seat is 150/month (120/month billed yearly) with 40,000 credits per seat per month. The pricing page shows dollars to US visitors and euros elsewhere, with the same amounts, excluding VAT.', + shortValue: '$30 or €30/seat/month ($24 or €24/month billed annually), 8,000 credits', confidence: 'verified', sources: [ { url: 'https://dust.tt/home/pricing', label: 'Dust Pricing', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, freeTier: { value: - 'Yes: a free Business tier for new workspaces, capped at 5 users, 3 connectors, and 5 Spaces, no credit card required', + 'Yes: a free Business option for new workspaces (no credit card, up to 5 users, 3 connectors, 5 Spaces); each Free seat gets a one-time allocation of 500 credits that does not renew', detail: - 'This free tier is what a new workspace gets by default without a paid subscription. It is distinct from what happens when an existing paid workspace downgrades: canceling removes all users except the earliest-assigned admin, deletes existing connections, and deletes data sources over 50MB combined after a 7-day warning period, while original source data in the connected provider itself is untouched.', - shortValue: 'Free for new workspaces: up to 5 users, 3 connectors, 5 Spaces', + 'The Free seat works as a trial: a member can hold it only once. Cancelling a paid subscription does not switch the workspace back to the free option; the subscription stays active until the end of the billing period, and by default Dust deletes the workspace data 30 days after that.', + shortValue: 'Free: up to 5 users, 500 one-time credits per seat', confidence: 'verified', sources: [ { url: 'https://dust.tt/home/pricing', label: 'Dust Pricing', - asOf: '2026-07-02', + asOf: '2026-10-15', }, { url: 'https://docs.dust.tt/docs/subscriptions', label: 'Subscriptions & Payments | Dust Docs', - asOf: '2026-07-04', + asOf: '2026-10-15', }, ], }, byok: { value: - 'No dedicated BYOK program: Dust bills usage via plan-included AI credits rather than a bring-your-own-provider-API-key option', + 'Not documented: Dust bills model usage through its own plan-included credits, and neither its pricing page nor its credits docs describe supplying your own provider API key', detail: - 'Pricing is structured around per-seat monthly credits that scale with model/task/tool complexity; no Dust source describes letting a workspace supply its own OpenAI/Anthropic API key in place of credit consumption.', + 'The pricing page states that all plans include access to 20+ models from OpenAI, Anthropic, Google, Mistral, and DeepSeek, consumed as Dust credits per message.', shortValue: 'Not documented; usage billed via included credits', - confidence: 'unknown', - sources: [], + confidence: 'estimated', + sources: [ + { + url: 'https://dust.tt/home/pricing', + label: 'Dust Pricing', + asOf: '2026-10-15', + }, + { + url: 'https://docs.dust.tt/docs/credits', + label: 'Credits | Dust Docs', + asOf: '2026-10-15', + }, + ], }, }, security: { diff --git a/apps/sim/lib/compare/data/competitors/gumloop.ts b/apps/sim/lib/compare/data/competitors/gumloop.ts index 936f979c68f..06edfdb6878 100644 --- a/apps/sim/lib/compare/data/competitors/gumloop.ts +++ b/apps/sim/lib/compare/data/competitors/gumloop.ts @@ -23,6 +23,26 @@ export const gumloopProfile: CompetitorProfile = { }, oneLiner: 'Gumloop is a hosted, no-code visual platform for building and deploying AI agents and automations: a drag-and-drop canvas, an AI copilot ("Gen") for natural-language flow creation, and native MCP (Model Context Protocol) integration support.', + leadAnswer: [ + "Gumloop is a hosted, proprietary no-code platform for building AI agents and automations, sold as a $37/month Pro plan or a custom Enterprise plan, with deployment into the customer's own cloud (VPC) as an optional Enterprise add-on. Sim is an open-source AI workspace with an Apache 2.0-licensed core for building, deploying, and managing agents visually, conversationally, or with code, available as a managed cloud or self-hosted with Docker Compose or Kubernetes. Choose Gumloop when you want a fully managed product aimed at non-technical business users, or a vendor-supported deployment inside your own cloud. Choose Sim when you need to run and inspect the platform yourself or want to start on a free plan.", + ], + faqs: [ + { + question: 'Can I self-host Gumloop agents, or do I need a platform like Sim?', + answer: + "Gumloop has no public self-hosted install: its app is proprietary, and the closest option is an optional Enterprise deployment into your own cloud (VPC). Sim's core is Apache 2.0 licensed and can be self-hosted with Docker Compose or Kubernetes (Helm), so your team runs the whole platform. On Gumloop's VPC option, compute is not billed in credits because it runs on your infrastructure, but model, tool-call, and orchestration charges still apply.", + }, + { + question: 'How do Sim and Gumloop compare for running AI agent workflows in production?', + answer: + "Sim gives each tool or API call configurable exponential-backoff retries (up to 10 attempts) and lets any block route failures to an error branch, while Gumloop documents no automatic retries or run replay and handles failures through a manual Error Shield node. Both alert on failures: Gumloop sends failure emails on Pro and above, and Sim's workspace-event trigger can push run failures and cost or latency spikes to Slack, email, or a webhook. Sim publishes its run ceilings (5-minute sync runs on Free, 50 minutes on paid plans, 90-minute async by default), while Gumloop publishes concurrency (5 concurrent runs on Pro) but no maximum run duration.", + }, + { + question: 'Is Gumloop open source, and is Sim?', + answer: + "Gumloop is not: its platform is proprietary, and its public GitHub repositories are client SDKs, forks, and an archived MCP-server project, not the app itself. Sim's core is open source under the Apache 2.0 license and can be self-hosted, with enterprise features such as SSO and audit logs under a separate license. That matters for teams that want to audit or modify the code their agents run on.", + }, + ], standoutFeatures: [ { title: '250+ fully hosted MCP servers', @@ -159,21 +179,21 @@ export const gumloopProfile: CompetitorProfile = { }, selfHostOption: { value: - 'No public self-host option for the core Gumloop app; enterprise customers can get a managed Virtual Private Cloud (VPC) deployment into their own cloud (e.g. GCP) instead of full self-hosting', + 'No public self-host option for the Gumloop app; Enterprise customers can optionally deploy it into their own cloud (VPC) in a region of their choice', detail: - "Gumloop deploys and operates the platform inside the customer's cloud project rather than offering a downloadable, self-managed open-source install. Gumloop's own guMCP_template repo is an open-source starter for self-hosted MCP servers, but it is not an install of the Gumloop app itself.", - shortValue: 'No self-host; VPC deployment only', - confidence: 'estimated', + 'There is no downloadable, self-managed install. VPC deployment is an optional Enterprise add-on, and VPC organizations are not charged credits for compute because it runs on their infrastructure.', + shortValue: 'No self-host; optional Enterprise VPC', + confidence: 'verified', sources: [ { url: 'https://www.gumloop.com/solutions/security', label: 'Gumloop Security & Trust', - asOf: '2026-07-02', + asOf: '2026-10-15', }, { - url: 'https://github.com/gumloop/guMCP_template', - label: "guMCP_template (Gumloop's self-hosted MCP starter repo)", - asOf: '2026-07-02', + url: 'https://docs.gumloop.com/core-concepts/credits', + label: 'Gumloop Docs: Credits (VPC compute)', + asOf: '2026-10-15', }, ], }, @@ -208,11 +228,15 @@ export const gumloopProfile: CompetitorProfile = { license: { value: 'Proprietary', detail: - "The core Gumloop application has no open-source license; it is a closed, hosted commercial SaaS product. Gumloop's own guMCP_template repo is an open-source MCP-server starter, but it is not the Gumloop platform.", + "The Gumloop application is closed, hosted commercial software. Gumloop's public GitHub repositories are client SDKs (gumloop-py, gumloop-js, Apache-2.0), forks, and an archived GPL-3.0 MCP-server project, not the platform itself.", shortValue: 'Proprietary', confidence: 'estimated', sources: [ - { url: 'https://www.gumloop.com/pricing', label: 'Gumloop Pricing', asOf: '2026-07-02' }, + { + url: 'https://github.com/gumloop', + label: 'Gumloop GitHub organization', + asOf: '2026-10-15', + }, ], }, environmentPromotion: { @@ -562,7 +586,7 @@ export const gumloopProfile: CompetitorProfile = { value: 'Partial: Loop Mode runs multiple list items concurrently (up to 15 at once on the Pro plan), but there is no documented dedicated node for splitting a single run into distinct parallel branches that later join, comparable to a fan-out/fan-in construct', detail: - 'Gumloop docs describe Loop Mode as processing list items simultaneously rather than one at a time, with concurrency limits tied to plan tier (2 concurrent items on Free, 15 on Pro). This is data-parallelism over a list, not branch-level fan-out/fan-in across different paths of logic.', + 'Gumloop docs describe Loop Mode as processing list items simultaneously rather than one at a time, with concurrency limits tied to plan tier (15 concurrent items on Pro). This is data-parallelism over a list, not branch-level fan-out/fan-in across different paths of logic.', shortValue: 'Partial: concurrent list-item processing (Loop Mode), no branch fan-out/fan-in node', confidence: 'estimated', @@ -596,7 +620,7 @@ export const gumloopProfile: CompetitorProfile = { }, loopIteration: { value: - "Partial: Gumloop's only documented iteration primitive is 'Loop Mode', the same mechanism covered under parallelExecution, which a user manually enables on a node so it runs once per item in a connected list. Per Gumloop's docs this is concurrent (2 items at once on Free, 15 on Pro), not a strictly one-at-a-time sequential container, and no separate while-loop or fixed-iteration-count node is documented, only manually-enabled iteration over an existing list.", + "Partial: Gumloop's only documented iteration primitive is 'Loop Mode', the same mechanism covered under parallelExecution, which a user manually enables on a node so it runs once per item in a connected list. Per Gumloop's docs this is concurrent (15 items at once on Pro), not a strictly one-at-a-time sequential container, and no separate while-loop or fixed-iteration-count node is documented, only manually-enabled iteration over an existing list.", detail: "Gumloop docs describe Loop Mode as a mode a user enables on a node ('When you enable Loop Mode on a node...'), which then processes multiple list items simultaneously with concurrency capped by plan tier, distinct from a classic for-each node that guarantees one iteration finishes before the next starts. No dedicated while-loop (condition-based) or fixed-count repeat node is documented; all iteration requires manually enabling Loop Mode with a list as input.", shortValue: 'Partial: manually-enabled Loop Mode is concurrent, not a sequential loop node', @@ -750,43 +774,76 @@ export const gumloopProfile: CompetitorProfile = { pricing: { pricingModel: { value: - 'Usage-based monthly credit system (credits consumed per task/run), with tiered monthly credit allotments from 5k (free) up to 1.5M+ on paid tiers, plus seat-based distinctions between Free/Pro/Enterprise', - shortValue: 'Credit-based usage across tiered plans', + 'Usage-based credits indexed at $0.005 each: Pro includes 20,000 credits per month and Enterprise is custom. Each agent run bills the list cost of model tokens, compute (5 credits per session-minute on the managed cloud; Enterprise VPC deployments are not charged for compute), and tool calls, plus an 8% orchestration fee', + detail: + 'Pro credits reset monthly and do not roll over (Enterprise credits do). Pro overage is capped at 1,000,000 credits per billing period by default. Enterprise plans can negotiate a different orchestration fee.', + shortValue: 'Credits at $0.005 each plus an 8% orchestration fee', confidence: 'verified', sources: [ - { url: 'https://www.gumloop.com/pricing', label: 'Gumloop Pricing', asOf: '2026-07-02' }, + { + url: 'https://www.gumloop.com/pricing', + label: 'Gumloop Pricing', + asOf: '2026-10-15', + }, + { + url: 'https://docs.gumloop.com/core-concepts/credits', + label: 'Gumloop Docs: Credits', + asOf: '2026-10-15', + }, ], }, entryPaidPlan: { value: - 'Pro plan at $37/month for 20k+ credits. Includes unlimited seats/teams, 5 concurrent runs, 25 concurrent agent interactions, agent reflections, unified billing, and 1 hosted MCP server instance', - detail: - "Gumloop's pricing page lists 'MCP Server Hosting (1)' under the Pro plan without clarifying its scope: it is not stated whether this cap limits access to the 100+ pre-built, zero-setup MCP servers described on gumloop.com/mcp, or only applies to a separate custom MCP server that Gumloop hosts on a customer's behalf. That distinction is not resolved anywhere on Gumloop's own pricing or MCP pages.", - shortValue: '$37/month Pro plan, 20k+ credits', + 'Pro plan at $37/month with 20,000 credits/month (7,400 at list price plus 12,600 bonus credits), unlimited seats and teams, 5 concurrent workflow runs, 25 concurrent agent chats, and 1 hosted MCP server', + shortValue: '$37/month Pro, 20,000 credits/month', confidence: 'verified', sources: [ - { url: 'https://www.gumloop.com/pricing', label: 'Gumloop Pricing', asOf: '2026-07-02' }, + { + url: 'https://www.gumloop.com/pricing', + label: 'Gumloop Pricing', + asOf: '2026-10-15', + }, + { + url: 'https://docs.gumloop.com/core-concepts/credits', + label: 'Gumloop Docs: Credits', + asOf: '2026-10-15', + }, ], }, freeTier: { value: - 'Yes: Free plan with 5,000 credits/month, 1 seat, 1 active trigger, 2 concurrent runs, 5 concurrent agent interactions, unlimited agents/flows, forum-only support', - shortValue: '5,000 credits/month, 1 seat', + 'No free plan: every new account gets a one-time 14-day free trial of Pro that requires a card and rolls into a paid $37/month Pro subscription unless cancelled', + detail: 'The plans table lists only Pro ($37/month) and Enterprise (custom).', + shortValue: 'No free plan; 14-day Pro trial, card required', confidence: 'verified', sources: [ - { url: 'https://www.gumloop.com/pricing', label: 'Gumloop Pricing', asOf: '2026-07-02' }, + { + url: 'https://docs.gumloop.com/core-concepts/credits', + label: 'Gumloop Docs: Credits (plans and trial)', + asOf: '2026-10-15', + }, + { + url: 'https://www.gumloop.com/pricing', + label: 'Gumloop Pricing', + asOf: '2026-10-15', + }, ], }, byok: { - value: 'Yes: Bring Your Own API Keys is supported across all plans', - shortValue: 'Supported on all plans', + value: + 'Yes, on Pro and Enterprise: with your own model-provider key, token credits drop to zero but the orchestration fee rises from 8% to 16% of what the run would have cost at list price', + shortValue: 'Pro and up; token credits waived, fee rises to 16%', confidence: 'verified', sources: [ - { url: 'https://www.gumloop.com/pricing', label: 'Gumloop Pricing', asOf: '2026-07-02' }, { - url: 'https://www.gumloop.com/solutions/security', - label: 'Gumloop Security & Trust', - asOf: '2026-07-02', + url: 'https://www.gumloop.com/pricing', + label: 'Gumloop Pricing (BYOK FAQ)', + asOf: '2026-10-15', + }, + { + url: 'https://docs.gumloop.com/core-concepts/credits', + label: 'Gumloop Docs: Credits (How BYOK works)', + asOf: '2026-10-15', }, ], }, @@ -1101,16 +1158,16 @@ export const gumloopProfile: CompetitorProfile = { }, executionLimits: { value: - 'Gumloop publishes concurrency limits by plan on its pricing page: Free allows 2 concurrent runs and 5 concurrent agent interactions, Pro allows 5 concurrent runs and 25 concurrent agent interactions, and Enterprise has custom, unpublished limits. Gumloop does not publicly document a maximum execution duration or per-request timeout for a single workflow run.', + 'Gumloop publishes concurrency limits by plan on its pricing page: Pro allows 5 concurrent workflow runs and 25 concurrent agent chats, and Enterprise limits are custom. Gumloop does not publicly document a maximum execution duration or per-request timeout for a single workflow run.', detail: - "Numbers taken directly from the pricing page comparison table. No max single-execution runtime or timeout figure is published in Gumloop's docs, forum, or pricing page.", - shortValue: '2-5 concurrent runs by plan; no published timeout', + "Numbers taken from the pricing page comparison table. No max single-execution runtime or timeout figure is published in Gumloop's docs or pricing page.", + shortValue: '5 concurrent runs on Pro; no published timeout', confidence: 'verified', sources: [ { url: 'https://www.gumloop.com/pricing', - label: 'Gumloop Pricing (concurrent runs / agent interactions by plan)', - asOf: '2026-07-02', + label: 'Gumloop Pricing (concurrency by plan)', + asOf: '2026-10-15', }, ], }, @@ -1153,11 +1210,11 @@ export const gumloopProfile: CompetitorProfile = { support: { supportChannels: { value: - 'Forum support on Free plan, escalating to dedicated Slack support on higher/Enterprise plans', - shortValue: 'Forum on Free, Slack on higher tiers', + 'Email support on Pro and dedicated Slack support on Enterprise, plus the Gumloop University learning resource', + shortValue: 'Email on Pro, dedicated Slack on Enterprise', confidence: 'estimated', sources: [ - { url: 'https://www.gumloop.com/pricing', label: 'Gumloop Pricing', asOf: '2026-07-02' }, + { url: 'https://www.gumloop.com/pricing', label: 'Gumloop Pricing', asOf: '2026-10-15' }, ], }, sla: { diff --git a/apps/sim/lib/compare/data/competitors/langflow.ts b/apps/sim/lib/compare/data/competitors/langflow.ts index c481c2f9548..67cca3df7f3 100644 --- a/apps/sim/lib/compare/data/competitors/langflow.ts +++ b/apps/sim/lib/compare/data/competitors/langflow.ts @@ -15,6 +15,26 @@ export const langflowProfile: CompetitorProfile = { }, oneLiner: 'Langflow is an open-source, Python-based visual builder for creating and deploying AI agents and RAG (retrieval-augmented generation) applications, owned by DataStax (an IBM company).', + leadAnswer: [ + "Langflow is an open-source, MIT-licensed, Python-based visual builder for AI agents and RAG applications, backed by IBM; since DataStax removed its hosted Langflow from Astra in April 2026, it runs on infrastructure you operate (Desktop, Docker, or Kubernetes). Sim is an open-source AI workspace with an Apache 2.0-licensed core for building, deploying, and managing agents visually, conversationally, or with code, offered as a managed cloud or self-hosted with Docker Compose or Kubernetes. Choose Langflow when you want to edit every component's Python source and work directly in the LangChain ecosystem. Choose Sim when you need documented production controls (failure alerts, error-handling branches, human approval steps, published execution limits) or a managed hosted plan.", + ], + faqs: [ + { + question: 'Which is more production-ready for agent workflows, Sim or Langflow?', + answer: + "Sim documents more production controls out of the box: alerts on failures and cost or latency spikes to Slack, email, or a webhook; error branches so one failing block does not halt a run; tool-call retries with exponential backoff; and published timeouts (50-minute sync runs on paid plans, 90-minute async by default). Langflow's public documentation does not describe failure alerting, per-step error routing, retries or replay, or execution limits for flows, though it captures per-step traces and can forward them to LangSmith, Langfuse, or LangWatch. In a self-hosted Langflow deployment, limits are whatever your own infrastructure and worker configuration allow.", + }, + { + question: 'How do Sim and Langflow compare for building agent workflows?', + answer: + "Both are visual canvas builders with a natural-language assistant, but Langflow leans developer-first: every component's Python source is editable, and its docs assume familiarity with LangChain concepts such as embeddings and vector stores. Sim offers a visual canvas, a Chat and Copilot surface that builds and edits workflows from plain-language requests, and code via API and SDK. Sim also has live multiplayer editing and a dedicated approval block that pauses a run for human sign-off, neither of which Langflow documents.", + }, + { + question: 'Is there still a hosted version of Langflow?', + answer: + 'DataStax removed its hosted Langflow from Astra on April 9, 2026, and pointed users to Langflow OSS, so the documented ways to run Langflow today are Langflow Desktop or your own Python, Docker, or Kubernetes deployment. IBM sells Elite Support for Langflow OSS and a watsonx Orchestrate integration, with no published pricing. Sim offers both a managed cloud with a Free plan and self-hosting with Docker Compose or Kubernetes.', + }, + ], standoutFeatures: [ { title: 'Deep LangChain/Python component ecosystem', @@ -114,26 +134,31 @@ export const langflowProfile: CompetitorProfile = { }, selfHostOption: { value: - 'Yes: Langflow is fully open source (MIT licensed) and can be self-hosted via pip/uv local install, Docker, or Kubernetes, in addition to a desktop app and Langflow Cloud.', + 'Yes: Langflow is open source (MIT) and self-hosted via a Python package (uv/pip), Docker, source, or Kubernetes Helm charts, or run locally as Langflow Desktop. The DataStax-hosted offering was removed from Astra on April 9, 2026', shortValue: 'Yes, self-hostable (pip, Docker, K8s)', confidence: 'verified', sources: [ { url: 'https://docs.langflow.org/get-started-installation', label: 'Langflow Docs: Install Langflow', - asOf: '2026-07-02', + asOf: '2026-10-15', + }, + { + url: 'https://docs.langflow.org/deployment-kubernetes-dev', + label: 'Langflow Docs: Kubernetes deployment', + asOf: '2026-10-15', }, { url: 'https://github.com/langflow-ai/langflow', label: 'GitHub: langflow-ai/langflow', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, deploymentOptions: { value: - 'Langflow can run as a desktop app on Windows/macOS, a local pip/uv install, a Docker container, or a Kubernetes deployment, plus a hosted Langflow Cloud option with a free account tier. Multi-worker setups are documented for scaling self-hosted instances.', - shortValue: 'Desktop, local, Docker/K8s, cloud, self-hosted', + 'Langflow can run as a desktop app on Windows/macOS, a local pip/uv install, a Docker container, or a Kubernetes deployment. Multi-worker setups are documented for scaling self-hosted instances. DataStax removed its hosted Langflow from Astra on April 9, 2026.', + shortValue: 'Desktop, local, Docker/K8s, self-hosted', confidence: 'verified', sources: [ { @@ -180,7 +205,7 @@ export const langflowProfile: CompetitorProfile = { { url: 'https://github.com/langflow-ai/langflow', label: 'GitHub: langflow-ai/langflow (License: MIT)', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -634,43 +659,55 @@ export const langflowProfile: CompetitorProfile = { pricing: { pricingModel: { value: - "Langflow's core software is free and open source, with no license fee for self-hosting. Third-party sources describe Langflow Cloud as offering a free account tier plus a paid tier around $25 per month for higher usage limits, and separate enterprise pricing; Langflow's official pricing page does not confirm these figures directly.", - detail: - "Based on third-party summaries; the official pricing page doesn't confirm these figures.", - shortValue: 'Free open-source core; cloud free tier plus paid/enterprise tiers', + "Free, open-source (MIT) software with no license fee and no published paid plan. DataStax's hosted Langflow was removed from Astra on April 9, 2026, and IBM's listed commercial options (IBM Elite Support for Langflow OSS and a watsonx Orchestrate integration) carry no public price", + shortValue: 'Free MIT software; no published paid plan', confidence: 'estimated', sources: [ { - url: 'https://www.lindy.ai/blog/langflow-pricing', - label: 'Lindy: Langflow Pricing', - asOf: '2026-07-02', + url: 'https://github.com/langflow-ai/langflow', + label: 'GitHub: langflow-ai/langflow (License: MIT)', + asOf: '2026-10-15', }, { - url: 'https://automationatlas.io/tools/langflow/', - label: 'Automation Atlas: Langflow pricing summary', - asOf: '2026-07-02', + url: 'https://docs.datastax.com/en/astra-db-serverless/release-notes.html#datastax-langflow-removal', + label: 'Astra release notes: DataStax Langflow removal (April 9, 2026)', + asOf: '2026-10-15', + }, + { + url: 'https://www.ibm.com/products/langflow', + label: 'IBM Langflow (Editions)', + asOf: '2026-10-15', }, ], }, entryPaidPlan: { value: - "Unknown: the exact entry paid-plan price and inclusions aren't confirmed on Langflow's own official pricing page. Third-party summaries cite a cloud paid tier starting around $25 per month.", - detail: 'Not confirmed against an official Langflow source.', - shortValue: 'Unverified; third parties cite roughly $25/month', - confidence: 'unknown', - sources: [], + 'None published: there is no Langflow-branded paid plan. The paid path IBM lists is IBM Elite Support for Langflow, aimed at enterprises needing SLAs and support for Langflow OSS, with no public price', + shortValue: 'No published paid plan', + confidence: 'estimated', + sources: [ + { + url: 'https://www.ibm.com/products/langflow', + label: 'IBM Langflow (Editions)', + asOf: '2026-10-15', + }, + ], }, freeTier: { value: - 'Yes: the open-source core is free to self-host, with no usage caps beyond your own infrastructure. Langflow Cloud reportedly offers a free account tier before infrastructure and API costs apply.', - detail: 'Exact cloud free-tier limits not officially confirmed.', - shortValue: 'Yes, free self-hosted core plus a free cloud tier', + 'Yes: the MIT-licensed software is free to self-host or to run as Langflow Desktop. A hosted free tier can no longer be confirmed, since DataStax removed its hosted Langflow from Astra on April 9, 2026', + shortValue: 'Free self-hosted and Desktop; no confirmed hosted tier', confidence: 'estimated', sources: [ { - url: 'https://www.lindy.ai/blog/langflow-pricing', - label: 'Lindy: Langflow Pricing', - asOf: '2026-07-02', + url: 'https://www.langflow.org', + label: 'Langflow homepage', + asOf: '2026-10-15', + }, + { + url: 'https://docs.datastax.com/en/astra-db-serverless/release-notes.html#datastax-langflow-removal', + label: 'Astra release notes: DataStax Langflow removal', + asOf: '2026-10-15', }, ], }, @@ -683,7 +720,7 @@ export const langflowProfile: CompetitorProfile = { { url: 'https://docs.langflow.org/agents', label: 'Langflow Docs: Use Langflow agents', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -948,9 +985,9 @@ export const langflowProfile: CompetitorProfile = { }, unattendedExecution: { value: - 'Partial: when Langflow is deployed as a server, self-hosted via Docker/Kubernetes or on Langflow Cloud, triggered runs (webhook, API call, or external cron/Airflow calling the API) execute on that server with no dependency on any particular client device staying open. But Langflow can also be run as a desktop app, and a flow triggered through that installation depends on the desktop app process itself staying running on that machine, since there is no separate always-on server component in that deployment mode.', + 'Partial: when Langflow is deployed as a server, self-hosted via Docker/Kubernetes, triggered runs (webhook, API call, or external cron/Airflow calling the API) execute on that server with no dependency on any particular client device staying open. But Langflow can also be run as a desktop app, and a flow triggered through that installation depends on the desktop app process itself staying running on that machine, since there is no separate always-on server component in that deployment mode.', detail: - 'The dependency is on the chosen deployment mode: a Docker/Kubernetes/Cloud deployment behaves like Sim (server-side, client-independent), while the desktop app is a local process that must stay running to serve triggers.', + 'The dependency is on the chosen deployment mode: a Docker/Kubernetes deployment behaves like Sim (server-side, client-independent), while the desktop app is a local process that must stay running to serve triggers.', shortValue: 'Yes if server-deployed; desktop app requires that app to stay running', confidence: 'estimated', sources: [ diff --git a/apps/sim/lib/compare/data/competitors/n8n.ts b/apps/sim/lib/compare/data/competitors/n8n.ts index 441e002b667..f8963e0fc1a 100644 --- a/apps/sim/lib/compare/data/competitors/n8n.ts +++ b/apps/sim/lib/compare/data/competitors/n8n.ts @@ -30,6 +30,27 @@ export const n8nProfile: CompetitorProfile = { }, oneLiner: 'n8n is a fair-code workflow automation platform combining a visual, node-based builder with custom code and built-in AI/agent nodes, available as a self-hosted or cloud-hosted product.', + leadAnswer: [ + 'n8n is a fair-code workflow automation platform with a visual node-based builder, custom code nodes, and built-in AI Agent nodes, available as a free self-hosted Community Edition or on n8n Cloud. Sim is an open-source AI workspace with an Apache 2.0-licensed core, where teams build, deploy, and manage AI agents visually, conversationally, or with code, on Sim Cloud or self-hosted. Choose n8n when you want execution-based pricing with unlimited users on every plan and dataset-based evaluations for AI workflows. Choose Sim when you want a permissively licensed core, a built-in hybrid-search knowledge base that syncs external sources, or live multiplayer editing of the same canvas.', + ], + faqs: [ + { + question: + 'n8n is fair-code with license restrictions. Is there a permissively licensed alternative?', + answer: + "Sim's core is licensed under Apache 2.0, a permissive OSI-approved license, while n8n's Sustainable Use License is not OSI open source. n8n's license permits use or modification only for internal business purposes or for non-commercial or personal use, and allows distribution only free of charge for non-commercial purposes. Both products keep enterprise features under a separate paid license: files marked '.ee.' in n8n, and the enterprise directory in Sim. Sim's Apache 2.0 core has no field-of-use limits and can be self-hosted with Docker Compose or Kubernetes.", + }, + { + question: "What is a good n8n alternative for teams that don't want to self-host?", + answer: + "Sim Cloud is a managed option that includes a Free plan, while n8n Cloud offers a trial with no credit card but no ongoing free cloud plan. Both are fully managed, so neither requires you to operate infrastructure. n8n's Business plan is currently available only for self-hosted n8n, so managed n8n customers choose among the Starter, Pro, and Enterprise cloud plans.", + }, + { + question: 'What does Sim do differently from n8n for building AI agents?', + answer: + "Sim builds a knowledge base into the workspace, while n8n assembles retrieval from separate nodes. n8n provides an AI Agent node that calls tools, including MCP tools, a RAG pipeline built from loaders, embeddings, and external vector stores such as Pinecone, Qdrant, or PGVector, and an Evaluations feature that scores workflows against test datasets. Sim provides agent blocks, a knowledge base with hybrid vector and full-text search plus connectors that continuously sync sources such as Google Drive and Confluence, and Chat and Copilot surfaces that build and edit workflows from natural language. Sim's Evaluator and Guardrails blocks score individual calls rather than running a batch dataset suite.", + }, + ], standoutFeatures: [ { title: 'Unlimited users on every plan, including the entry tier', @@ -161,7 +182,7 @@ export const n8nProfile: CompetitorProfile = { { url: 'https://docs.n8n.io/choose-how-to-use-n8n.md', label: 'n8n docs: Choose how to use n8n', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -210,7 +231,7 @@ export const n8nProfile: CompetitorProfile = { { url: 'https://raw.githubusercontent.com/n8n-io/n8n/master/LICENSE.md', label: 'n8n LICENSE.md', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -814,51 +835,73 @@ export const n8nProfile: CompetitorProfile = { pricingModel: { value: 'Per-workflow-execution pricing tiers, unlimited users at every tier', detail: - "n8n's pricing page states workflows are billed by monthly execution count. A workflow that runs start-to-finish counts as one execution regardless of the number of steps or nodes. Unlimited users are included at every paid tier.", + "n8n's pricing page states that an execution is a single run of an entire workflow, regardless of how many steps it has or how much data it processes, and that all plans include unlimited users and workflows. The Business plan is currently only available for self-hosted n8n.", shortValue: 'Billed by monthly workflow executions', confidence: 'verified', - sources: [{ url: 'https://n8n.io/pricing/', label: 'n8n Pricing', asOf: '2026-07-02' }], + sources: [ + { + url: 'https://n8n.io/pricing/', + label: 'n8n Pricing', + asOf: '2026-10-15', + }, + ], }, entryPaidPlan: { value: 'Starter plan: €20/month (billed annually), 2,500 executions/month, cloud-hosted', detail: - 'Starter includes 2,500 workflow executions per month, 5 concurrent executions, 1 shared project, unlimited users, 2,300 AI credits, and forum support.', + 'Starter includes 2,500 workflow executions per month, 5 concurrent executions, 1 shared project, unlimited users and workflows, 1,600 Assistant credits per month, and forum support.', shortValue: '€20/month, 2,500 executions', confidence: 'verified', - sources: [{ url: 'https://n8n.io/pricing/', label: 'n8n Pricing', asOf: '2026-07-08' }], + sources: [ + { + url: 'https://n8n.io/pricing/', + label: 'n8n Pricing', + asOf: '2026-10-15', + }, + ], }, freeTier: { - value: 'Yes: free self-hosted Community Edition, plus a free cloud trial (no credit card)', + value: + 'Yes: free self-hosted Community Edition, plus a free n8n Cloud trial (no credit card); no free cloud plan', detail: - "The Community Edition is a free, self-hosted version of n8n runnable on a user's own infrastructure. Separately, n8n Cloud offers a free trial of the Starter/Pro plans without requiring a credit card.", + 'The Community Edition is a free, self-hosted version of n8n with almost the complete feature set. n8n Cloud offers a trial with 1,000 executions and 5 concurrent executions with no credit card required; there is no ongoing free cloud plan.', shortValue: 'Free self-hosted edition and cloud trial', confidence: 'verified', sources: [ { url: 'https://docs.n8n.io/choose-how-to-use-n8n.md', label: 'n8n docs: Choose how to use n8n', - asOf: '2026-07-02', + asOf: '2026-10-15', + }, + { + url: 'https://n8n.io/pricing/', + label: 'n8n Pricing', + asOf: '2026-10-15', }, - { url: 'https://n8n.io/pricing/', label: 'n8n Pricing', asOf: '2026-07-02' }, ], }, byok: { value: - 'De facto yes: Chat Model nodes require users\' own provider API credentials, though n8n does not name this "BYOK"', + 'Yes: self-hosted n8n is BYOK with no credit limits; n8n Cloud Starter and Pro also offer prepaid Gateway credits to use supported models without your own API keys', detail: - "n8n's OpenAI and Anthropic credential docs both walk users through generating an API key in the provider's own console and entering it as the node credential; no n8n-hosted key is supplied for these Chat Model nodes. Separately, n8n's pricing page describes plan-included 'AI credits' (2,300 on Starter, up to 13,700+ on higher tiers) for n8n's own in-app AI Assistant feature, which is distinct from the provider credentials Chat Model nodes require. n8n does not name a bring-your-own-API-key policy, but requiring each provider's own key for Chat Model nodes makes BYOK the de facto default for workflow-level LLM calls.", - shortValue: 'De facto via provider API keys, not named', - confidence: 'estimated', + "n8n's pricing page states that on self-hosted n8n you bring your own key (BYOK), so no credit limits apply. On n8n Cloud Starter and Pro, Chat Model nodes can use either your own provider credential or Gateway credits, a prepaid balance that runs supported AI models and services without provider accounts; Gateway credits are not available on Cloud Enterprise or self-hosted n8n. Assistant credits are a separate allowance for n8n's in-app Assistant.", + shortValue: 'BYOK; Cloud also offers keyless Gateway credits', + confidence: 'verified', sources: [ { - url: 'https://docs.n8n.io/integrations/builtin/credentials/openai', - label: 'n8n docs: OpenAI credentials', - asOf: '2026-07-08', + url: 'https://n8n.io/pricing/', + label: 'n8n Pricing', + asOf: '2026-10-15', }, { - url: 'https://docs.n8n.io/integrations/builtin/credentials/anthropic', - label: 'n8n docs: Anthropic credentials', - asOf: '2026-07-08', + url: 'https://docs.n8n.io/deploy/use-n8n-cloud/gateway-credits.md', + label: 'n8n docs: Gateway credits', + asOf: '2026-10-15', + }, + { + url: 'https://docs.n8n.io/integrations/builtin/credentials/openai', + label: 'n8n docs: OpenAI credentials', + asOf: '2026-10-15', }, ], }, diff --git a/apps/sim/lib/compare/data/competitors/openclaw.ts b/apps/sim/lib/compare/data/competitors/openclaw.ts index ad3b531b36b..b1757dfc642 100644 --- a/apps/sim/lib/compare/data/competitors/openclaw.ts +++ b/apps/sim/lib/compare/data/competitors/openclaw.ts @@ -16,6 +16,27 @@ export const openClawProfile: CompetitorProfile = { }, oneLiner: "OpenClaw is a free, open-source, self-hosted personal AI agent that runs on a user's own machine or server and connects to messaging platforms (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, and others) as its primary interface, extensible via a Skills plugin system and the ClawHub marketplace. It is not a visual workflow/automation builder like Sim, n8n, or Power Automate.", + leadAnswer: [ + 'OpenClaw is a free, MIT-licensed personal AI agent that runs as a single self-hosted Gateway process on your own machine or server and is operated through chat apps such as WhatsApp, Telegram, Slack, and Discord. Sim is an open-source AI workspace where teams build, deploy, and manage agents on a visual canvas, in natural language, or with code, on Sim Cloud or a self-hosted Docker or Kubernetes deployment. Choose OpenClaw for a single-operator assistant you message from your existing chat apps, using your own model keys and keeping state on your own hardware. Choose Sim when a team needs shared workspaces with role-based permissions, per-block execution traces, and scheduled or webhook-triggered runs that execute server-side without depending on one machine staying on.', + ], + faqs: [ + { + question: + 'What is the best alternative to OpenClaw for teams that need production reliability?', + answer: + 'Sim is an OpenClaw alternative where scheduled, webhook, and chat-triggered runs execute as background jobs on the Sim server, whether Sim Cloud or your own self-hosted deployment, so no personal machine has to stay on. OpenClaw automations run inside its self-hosted Gateway process, and its docs state the Gateway must be running for schedules to fire. Sim also records a per-block trace of every run (duration, cost, tokens) and can alert Slack, email, or a webhook on failures or cost and latency spikes. OpenClaw keeps automation run history for 7 days and, by default, sends a failure alert after 2 consecutive failures.', + }, + { + question: 'What is a good OpenClaw alternative for building production AI agents?', + answer: + 'Sim is an OpenClaw alternative for teams that build agents as workflows and publish them as REST APIs, MCP servers, scheduled jobs, or chat interfaces. OpenClaw is a personal assistant configured through a JSON file and Markdown Skills and operated by messaging it, with no visual builder. In Sim, any block can route to an error branch so one failing step does not halt the run, and on the Enterprise plan a workspace can be forked into dev, QA, and prod copies and promoted between them.', + }, + { + question: 'Sim vs OpenClaw for building AI agents: which should I use?', + answer: + "Use OpenClaw if you want one personal agent that you message from WhatsApp, Telegram, Slack, Discord, or another chat app, running on your own hardware with your own model keys. Use Sim if several people need to build and run agents together: Sim has admin, write, and read workspace permissions and live multiplayer editing of the same workflow, while OpenClaw's security docs assume one trusted boundary per gateway and state it is not a hostile multi-tenant security boundary. OpenClaw is free under the MIT license; Sim's core is Apache 2.0, with a Free plan and a Pro plan at $25 per user per month.", + }, + ], standoutFeatures: [ { title: '22+ messaging channels as the primary interface', @@ -197,14 +218,19 @@ export const openClawProfile: CompetitorProfile = { value: "Yes: self-hosting is the only deployment model. OpenClaw runs as a single Gateway process on the user's own machine or server. There is no OpenClaw-operated hosted/SaaS version.", detail: - 'Docs describe the Gateway as running locally and being "the single source of truth for sessions, routing, and channel connections," with a local workspace at ~/.openclaw/workspace.', + 'The docs say you "run a single Gateway process on your own machine (or a server)" with "No hosted service in the middle", and the homepage says "State lives on your machine, not a vendor cloud." OpenClaw Enterprise (announced September 2026) is also self-hosted only, deployed with docker-compose or Kubernetes on the organization\'s own infrastructure.', shortValue: 'Yes, self-hosting only, no vendor-hosted SaaS option', confidence: 'verified', sources: [ { url: 'https://docs.openclaw.ai/', label: 'OpenClaw Docs home', - asOf: '2026-07-02', + asOf: '2026-10-15', + }, + { + url: 'https://openclaw.ai/blog/openclaw-enterprise/', + label: 'OpenClaw blog: OpenClaw Enterprise', + asOf: '2026-10-15', }, ], }, @@ -242,19 +268,24 @@ export const openClawProfile: CompetitorProfile = { value: 'MIT License (permissive open source), stewarded by the independent, non-profit OpenClaw Foundation, not a single vendor company', detail: - "The GitHub repository's LICENSE file confirms MIT licensing. Governance passed from creator Peter Steinberger to a community-elected Foundation board after he joined OpenAI in February 2026. OpenAI is a Foundation sponsor (inference support and Codex Security scanning) but does not own the project.", + 'The LICENSE file reads "MIT License, Copyright (c) 2026 OpenClaw Foundation." The homepage describes the Foundation as "an independent US 501(c)(3) non-profit" that keeps "the whole product MIT licensed". Its institutional donors include Microsoft, NVIDIA, OpenAI, Red Hat, and the University of Michigan.', shortValue: 'MIT license, non-profit Foundation governance', confidence: 'verified', sources: [ { url: 'https://github.com/openclaw/openclaw/blob/main/LICENSE', label: 'openclaw/openclaw LICENSE file', - asOf: '2026-07-02', + asOf: '2026-10-15', }, { - url: 'https://openclaw.ai/ecosystem/', - label: 'OpenClaw Ecosystem page', - asOf: '2026-07-02', + url: 'https://openclaw.ai/', + label: 'OpenClaw homepage', + asOf: '2026-10-15', + }, + { + url: 'https://openclaw.org/donors', + label: 'OpenClaw Foundation donors', + asOf: '2026-10-15', }, ], }, @@ -763,7 +794,7 @@ export const openClawProfile: CompetitorProfile = { { url: 'https://github.com/openclaw/openclaw/blob/main/LICENSE', label: 'openclaw/openclaw LICENSE file', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -778,7 +809,7 @@ export const openClawProfile: CompetitorProfile = { { url: 'https://github.com/openclaw/openclaw/blob/main/LICENSE', label: 'openclaw/openclaw LICENSE file', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -793,22 +824,27 @@ export const openClawProfile: CompetitorProfile = { { url: 'https://github.com/openclaw/openclaw/blob/main/LICENSE', label: 'openclaw/openclaw LICENSE file', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, byok: { value: - 'Yes, and mandatory: OpenClaw requires the operator to supply their own API credentials/OAuth login for whichever model provider(s) they configure (Anthropic, OpenAI, Google, or any OpenAI-compatible endpoint); there is no OpenClaw-hosted model access.', + 'Yes, and it is the only option: model access always comes from the operator, via their own provider API key, a provider OAuth login (e.g. OpenAI ChatGPT/Codex), a third-party gateway such as OpenRouter, or a local runtime (Ollama, LM Studio, vLLM, llama.cpp). OpenClaw provides no hosted model access of its own.', detail: - 'Onboarding documentation walks through provider-specific auth flows (e.g. `openclaw models auth paste-token --provider anthropic`, `openclaw models auth login --provider openai-codex`). BYOK is the only supported model-access model.', - shortValue: 'Yes, mandatory; no OpenClaw-hosted model access', + 'The model-providers docs cover multiple API keys with rotation under Settings -> Models, provider OAuth login, and onboarding commands such as `openclaw onboard --auth-choice opencode-zen`. The homepage states "No subscription. No hosted tier. No token."', + shortValue: 'Yes, only option; own keys, OAuth, gateway, or local model', confidence: 'verified', sources: [ { url: 'https://docs.openclaw.ai/concepts/model-providers', label: 'OpenClaw Docs: Model providers', - asOf: '2026-07-02', + asOf: '2026-10-15', + }, + { + url: 'https://openclaw.ai/', + label: 'OpenClaw homepage', + asOf: '2026-10-15', }, ], }, diff --git a/apps/sim/lib/compare/data/competitors/pipedream.ts b/apps/sim/lib/compare/data/competitors/pipedream.ts index ebfce7c7a49..c127f49ef7c 100644 --- a/apps/sim/lib/compare/data/competitors/pipedream.ts +++ b/apps/sim/lib/compare/data/competitors/pipedream.ts @@ -18,7 +18,27 @@ export const pipedreamProfile: CompetitorProfile = { asOf: '2026-07-02', }, oneLiner: - 'Pipedream is a cloud integration platform, now owned by Workday (acquired Nov 2025), that connects 3,000+ APIs through low-code workflows or custom Node.js/Python/Go code, and exposes those integrations to AI agents as tools through a hosted MCP server.', + 'Pipedream is a cloud integration platform, now owned by Workday (acquired Nov 2025), whose Connect product gives apps and AI agents managed authentication and prebuilt tools across 3,000+ APIs through SDKs, a REST API, and a hosted MCP server; its Workflows automation product no longer accepts signups and shuts down on March 31, 2027.', + leadAnswer: [ + 'Pipedream is an integration layer for AI agents: its Connect product gives developers managed authentication and prebuilt tools across 3,000+ APIs through SDKs, a REST API, and an MCP server, while its Workflows automation product is in maintenance mode and shuts down on March 31, 2027. Sim is an open-source AI workspace with an Apache 2.0-licensed core, where teams build, deploy, and manage AI agents and automations visually, conversationally, or with code, on Sim Cloud or self-hosted. Choose Pipedream Connect when you are a developer embedding customer-facing integrations or tools into your own app or agent. Choose Sim when you want to build and run the automations and agents themselves on a visual canvas, including self-hosted.', + ], + faqs: [ + { + question: 'What is a Pipedream alternative with a visual builder for non-developers?', + answer: + "Sim offers a visual drag-and-drop canvas and a natural-language Chat surface that builds and edits workflows from plain-language requests. Pipedream's visual builder belongs to its Workflows product, which no longer accepts signups or subscriptions and shuts down on March 31, 2027. Pipedream's ongoing product, Connect, is delivered as SDKs, a REST API, and an MCP server for developers adding integrations to their own apps and agents.", + }, + { + question: 'How does Sim compare to Pipedream for building automations?', + answer: + 'Sim is built to create and run automations and agents, while Pipedream now centers on Connect, an integration layer that supplies tools and managed authentication to apps and agents you build yourself. Sim workflows run from webhook, schedule, chat, REST API, and app-event triggers, and can include code blocks for custom logic. Pipedream Workflows supported low-code steps plus custom Node.js, Python, Bash, or Go code, but it is in maintenance mode with no new features and ends on March 31, 2027.', + }, + { + question: 'Is Pipedream Workflows shutting down?', + answer: + 'Yes. Pipedream Workflows shuts down on March 31, 2027, and all Workflows data, including stored credentials and OAuth tokens, is deleted by April 30, 2027; Pipedream Connect is not affected. Pipedream provides an export tool that downloads workflow structure as JSON, one project at a time. Sim, with an Apache 2.0-licensed core, can run those automations on Sim Cloud or self-hosted, though workflows need to be rebuilt in Sim rather than imported.', + }, + ], standoutFeatures: [ { title: 'Hosted MCP server covering thousands of apps', @@ -34,9 +54,14 @@ export const pipedreamProfile: CompetitorProfile = { { title: 'Code-first escape hatch alongside no-code builder', description: - "Any step in a Pipedream workflow can be replaced with custom Node.js, Python, Bash, or Go code, and pre-built actions can be used as no-code building blocks or 'scaffolded' into code you customize.", - shortDescription: 'Every workflow step can drop into custom code in four languages.', - source: { url: 'https://pipedream.com/docs', label: 'Pipedream Docs', asOf: '2026-07-02' }, + "Any step in a Pipedream workflow can be replaced with custom Node.js, Python, Bash, or Go code, and pre-built actions can be used as no-code building blocks or 'scaffolded' into code you customize. This is part of Pipedream Workflows, which shuts down on March 31, 2027.", + shortDescription: + 'Workflow steps drop into custom code in four languages (Workflows, ending 2027).', + source: { + url: 'https://pipedream.com/docs/workflows', + label: 'Pipedream Docs: Workflows', + asOf: '2026-10-15', + }, }, { title: 'Source-available component registry on GitHub', @@ -51,6 +76,17 @@ export const pipedreamProfile: CompetitorProfile = { }, ], limitations: [ + { + title: 'Workflows product is shutting down', + description: + 'Pipedream Workflows is in maintenance mode, no longer accepts signups or subscriptions, and shuts down on March 31, 2027. All Workflows data, including stored credentials and OAuth tokens, is deleted by April 30, 2027; an export tool saves workflow structure as JSON, one project at a time. Pipedream Connect is not affected.', + shortDescription: 'Workflows shuts down on March 31, 2027; Connect continues.', + source: { + url: 'https://pipedream.com/docs/workflows', + label: 'Pipedream Docs: Workflows', + asOf: '2026-10-15', + }, + }, { title: 'Not self-hostable', description: @@ -65,8 +101,8 @@ export const pipedreamProfile: CompetitorProfile = { { title: 'Proprietary source-available license, not open source', description: - "The GitHub repository is licensed under the 'Pipedream Source Available License Version 1.0,' which explicitly prohibits using the code to run a competing SaaS/PaaS/IaaS offering. This is not an OSI-approved open-source license.", - shortDescription: "The component repo's license bars running a competing service.", + "The GitHub repository is licensed under the 'Pipedream Source Available License Version 1.0,' whose Excluded Purpose covers any commercial use of the code, including running a competing SaaS/PaaS/IaaS offering. This is not an OSI-approved open-source license.", + shortDescription: "The component repo's license excludes commercial use.", source: { url: 'https://github.com/PipedreamHQ/pipedream/blob/master/LICENSE', label: 'GitHub: LICENSE file', @@ -126,12 +162,12 @@ export const pipedreamProfile: CompetitorProfile = { { url: 'https://github.com/PipedreamHQ/pipedream/issues/954', label: 'GitHub Issue #954', - asOf: '2026-07-02', + asOf: '2026-10-15', }, { url: 'https://github.com/PipedreamHQ/pipedream', label: 'GitHub: PipedreamHQ/pipedream repo description', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -172,14 +208,14 @@ export const pipedreamProfile: CompetitorProfile = { value: 'Proprietary (Pipedream Source Available License v1.0) for the GitHub component registry; hosted platform is closed-source SaaS', detail: - 'Not an OSI-approved open-source license. It bars using the code to run a competing SaaS/PaaS/IaaS.', + 'Not an OSI-approved open-source license. Its Excluded Purpose covers any commercial use of the software, including running a SaaS, PaaS, IaaS, or other online service that competes with Pipedream.', shortValue: 'Proprietary source-available registry license', confidence: 'verified', sources: [ { url: 'https://github.com/PipedreamHQ/pipedream/blob/master/LICENSE', label: 'GitHub: LICENSE file', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -681,51 +717,65 @@ export const pipedreamProfile: CompetitorProfile = { pricing: { pricingModel: { value: - 'Credit-based compute pricing (per-execution-time credits) plus separate Connect usage/end-user pricing', + 'Credit-based Connect usage (1 credit per 30 seconds of compute) plus a per-external-user fee; legacy Workflows credit pricing ends with the product on March 31, 2027', detail: - '1 credit = 30 seconds of workflow compute at 256MB memory; credit burn rate scales with memory allocation. Connect (embedding Pipedream in your own app) bills separately on API usage (actions/tool calls/trigger emits) and number of connected end users.', - shortValue: 'Credit-based compute; separate Connect usage pricing', + "Pipedream's pricing page now covers Connect only: credits are consumed by action runs, MCP tool calls, Connect proxy requests, and deployed-trigger emits at 1 credit per 30 seconds of compute, and plans also bill per unique external user who connects an account. Pipedream Workflows used the same 30-second credit at 256MB memory, but Workflows no longer accepts signups or subscriptions and shuts down on March 31, 2027.", + shortValue: 'Connect credits plus per-user fee', confidence: 'verified', sources: [ + { + url: 'https://pipedream.com/pricing', + label: 'Pipedream Pricing', + asOf: '2026-10-15', + }, { url: 'https://pipedream.com/docs/pricing', label: 'Pipedream Docs: Plans and Pricing', - asOf: '2026-07-02', + asOf: '2026-10-15', + }, + { + url: 'https://pipedream.com/docs/workflows', + label: 'Pipedream Docs: Workflows', + asOf: '2026-10-15', }, ], }, entryPaidPlan: { value: - 'Not publicly listed; third-party estimates range roughly $29–$45/mo for the entry paid tier', + 'Startup plan: $99/month billed annually ($150 billed monthly), 10,000 usage credits and 100 external users included', detail: - "Pipedream's own pricing page is JavaScript-rendered and not directly verifiable via static fetch. Third-party sources report conflicting numbers for the entry paid tier, roughly $29/mo to $45/mo for around 2,000 credits/month, but the exact current price is unconfirmed from a primary source.", - shortValue: '~$29–$45/mo (unconfirmed)', - confidence: 'estimated', + 'Startup is the entry paid plan and is required to run Connect in production. It includes 10,000 usage credits per month (then $0.012 each) and 100 external users (then $2 per user per month), with email support. Pipedream Workflows subscriptions are no longer sold.', + shortValue: '$99/month annual, 10,000 credits', + confidence: 'verified', sources: [ { url: 'https://pipedream.com/pricing', - label: - 'Pipedream Pricing page (official, JS-rendered, not independently verifiable via fetch)', - asOf: '2026-07-02', + label: 'Pipedream Pricing', + asOf: '2026-10-15', }, { - url: 'https://automationatlas.io/answers/pipedream-pricing-explained-2026/', - label: 'Automation Atlas: Pipedream Pricing Explained 2026 (third-party)', - asOf: '2026-07-02', + url: 'https://pipedream.com/docs/workflows', + label: 'Pipedream Docs: Workflows', + asOf: '2026-10-15', }, ], }, freeTier: { - value: 'Yes: free workspace with a daily credit limit', + value: 'Yes: free workspace for development only, with a daily credit limit', detail: - 'Free workspaces get a daily limit of free credits (cannot be exceeded/rolled over), a capped number of active workflows and connected accounts, community-only support, and Connect development-environment access only. Developing/testing a workflow with test events in the builder is always free, and public-registry event source triggers are free.', - shortValue: 'Free tier with daily credit cap', + 'The Free plan ($0, no credit card) includes every feature in development, unlimited team members, and community support, with a daily credit limit that cannot be exceeded and a limited number of connected accounts. Serving real external users in production requires the paid Startup plan.', + shortValue: 'Free in development, daily credit cap', confidence: 'verified', sources: [ + { + url: 'https://pipedream.com/pricing', + label: 'Pipedream Pricing', + asOf: '2026-10-15', + }, { url: 'https://pipedream.com/docs/pricing', label: 'Pipedream Docs: Plans and Pricing', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, diff --git a/apps/sim/lib/compare/data/competitors/vellum.ts b/apps/sim/lib/compare/data/competitors/vellum.ts index 1b9046b0dfc..475e036e7c3 100644 --- a/apps/sim/lib/compare/data/competitors/vellum.ts +++ b/apps/sim/lib/compare/data/competitors/vellum.ts @@ -16,6 +16,26 @@ export const vellumProfile: CompetitorProfile = { }, oneLiner: 'Vellum is an enterprise AI development platform for building, evaluating, and deploying LLM prompts, workflows, and agents.', + leadAnswer: [ + "Vellum's enterprise platform is a hosted AI development product for building, evaluating, and deploying LLM prompts, workflows, and agents through a visual builder, a code SDK, or natural language, with self-hosted and VPC installs for enterprises; its public website now leads with a separate consumer assistant, and the platform's pricing is not published. Sim is an open-source AI workspace with an Apache 2.0-licensed core for building, deploying, and managing agents visually, conversationally, or with code, with a Free plan, a $25 per user/month Pro plan, and self-hosting via Docker Compose or Kubernetes. Choose Vellum when test-suite evaluations, HIPAA coverage with a signed BAA, or a vendor-managed single-tenant VPC are requirements. Choose Sim when you want published self-serve pricing, an open-source core, or live multiplayer editing of the same workflow.", + ], + faqs: [ + { + question: 'Which is easier for non-engineers to use, Sim or Vellum?', + answer: + "Both offer a natural-language path: Vellum turns a plain-language goal into a working agent, and Sim's Chat and in-editor Copilot build and edit workflows from natural-language requests on top of a visual canvas. The practical difference is access: Sim has a Free plan and a published $25 per user/month Pro plan, while Vellum's enterprise platform publishes no pricing and its self-hosted option is arranged through a sales demo.", + }, + { + question: 'How do Sim and Vellum compare for building and deploying agents?', + answer: + 'Both build agents with a visual builder, code, or natural language, and both deploy to managed cloud or self-hosted infrastructure; Vellum adds a vendor-managed VPC install on AWS, Azure, or GCP, while Sim self-hosts with Docker Compose or Kubernetes. For promotion, Vellum offers Dev, Staging, and Prod environments with one-click promotion, and Sim (on the Enterprise plan) forks a whole workspace into a dev/qa/prod-style child, diffs it, and requires credential remapping before each promote. Sim also deploys a workflow as a public chat interface or as a tool on an MCP server.', + }, + { + question: 'Is Vellum still an enterprise agent platform or a personal assistant?', + answer: + "Both exist under one brand: vellum.ai, including its pricing page, now presents a consumer 'Personal Intelligence' assistant with $30 to $200 monthly packages, while docs.vellum.ai still documents the enterprise workflow and evaluation platform. The most recent entries in those platform docs are a February 2026 changelog and a self-hosted release dated February 6, 2026. Sim is a single product, an AI workspace for building and deploying agents, with its plans published on its own pricing page.", + }, + ], standoutFeatures: [ { title: 'HIPAA compliance with a signable BAA', @@ -54,15 +74,15 @@ export const vellumProfile: CompetitorProfile = { }, }, { - title: 'Enterprise BYOK policy no longer verifiable', + title: 'Enterprise pricing no longer published', description: - "Vellum's pricing page has been replaced by a consumer 'Personal Intelligence' pricing model (subscription plus pay-as-you-go credits); the original enterprise LLM-cost-pass-through pricing this claim describes, including any bring-your-own-API-key policy, is no longer published at this URL.", + "Vellum's pricing page now shows a consumer 'Personal Intelligence' product (subscription plus pay-as-you-go credits). The enterprise platform's pricing, including how your own provider API keys affect billing, is no longer published.", shortDescription: - 'Pricing page now shows a different consumer product; enterprise BYOK policy unconfirmed.', + 'Pricing page now shows a different consumer product; enterprise pricing unpublished.', source: { url: 'https://www.vellum.ai/pricing', label: 'Vellum Pricing (now shows the consumer product)', - asOf: '2026-07-08', + asOf: '2026-10-15', }, }, { @@ -122,19 +142,19 @@ export const vellumProfile: CompetitorProfile = { selfHostOption: { value: 'Yes: self-hosted / VPC install available for enterprise customers', detail: - "A 'Self-Hosted Vellum' path and a VPC Install option (via a Replicated-based deployment) let enterprises run the platform in their own AWS/Azure/GCP VPC or on-prem.", + "Vellum's self-hosting docs describe running 'the complete Vellum platform within your own infrastructure', including air-gapped environments, available through a sales demo. A 2024 announcement describes VPC installs on AWS, Azure, and GCP supported via Replicated. The most recent self-hosted release listed is v0.3.44 (February 6, 2026).", shortValue: 'Self-hosted / VPC for enterprise', confidence: 'estimated', sources: [ { url: 'https://docs.vellum.ai/self-hosting/getting-started/introduction', label: 'Self-Hosted Vellum: Vellum Docs', - asOf: '2026-07-02', + asOf: '2026-10-15', }, { url: 'https://www.vellum.ai/blog/announcing-vellum-vpc', label: 'Announcing Vellum VPC', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -166,14 +186,14 @@ export const vellumProfile: CompetitorProfile = { value: "Proprietary/commercial for the core enterprise Vellum platform. A separate consumer 'Vellum Assistant' product is open-sourced under MIT license.", detail: - 'github.com/vellum-ai/vellum-assistant is MIT-licensed (confirmed live, 825+ stars). No evidence the enterprise workflow/evaluations platform itself is open source.', + 'github.com/vellum-ai/vellum-assistant is MIT-licensed (1,400+ stars). No evidence the enterprise workflow/evaluations platform itself is open source.', shortValue: 'Proprietary enterprise platform; MIT consumer app', confidence: 'estimated', sources: [ { url: 'https://github.com/vellum-ai/vellum-assistant', label: 'vellum-ai/vellum-assistant: GitHub', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -697,19 +717,19 @@ export const vellumProfile: CompetitorProfile = { value: "Not publicly listed for the B2B enterprise platform: Vellum's current pricing page has been fully replaced by the unrelated consumer 'Personal Intelligence' product's plans. A September 2025 third-party pricing analysis, published before that switch, described the enterprise platform as execution-credit-based with per-tier seat caps (Free, Pro, Enterprise) rather than the prepaid-credit model now shown.", detail: - "Current pricing pages (vellum.ai/pricing, vellum.ai/docs/pricing) describe the consumer 'Personal Intelligence' product's plans (Base/Free and Pro $50/mo tiers with configurable vCPU/RAM/storage add-ons), not the enterprise workflow platform. The last third-party description of the enterprise platform's model itself states Vellum 'does not publicly list pricing details on its website,' so the figures below are third-party estimates, not Vellum-published prices.", + "Current pricing pages (vellum.ai/pricing, vellum.ai/docs/pricing) describe the consumer 'Personal Intelligence' assistant: a free Base plan and Pro packages at $30, $100, and $200 per month or a Custom configuration from $50/month. None of this covers the enterprise workflow platform. The last third-party description of the enterprise platform (ZenML, September 2025) states Vellum 'does not publicly list pricing details on its website,' so the figures below are third-party estimates, not Vellum-published prices.", shortValue: 'Not publicly listed; third-party analysis describes credit-based tiers', confidence: 'unknown', sources: [ { url: 'https://www.zenml.io/blog/vellum-ai-pricing', label: 'Vellum AI Pricing: ZenML Blog', - asOf: '2026-07-04', + asOf: '2026-10-15', }, { url: 'https://www.vellum.ai/pricing', label: 'Vellum Pricing (now shows the consumer product)', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -717,14 +737,14 @@ export const vellumProfile: CompetitorProfile = { value: 'Unconfirmed: a third-party analysis reports a Pro tier around $500/month for the enterprise platform, but Vellum does not publish this figure itself', detail: - "The cited third-party breakdown describes Pro at roughly $500/month with 5,000 prompt executions/day, 250 workflow executions/day, RBAC, and monitoring integrations, still capped at 5 users; Enterprise tiers above that are custom/'Contact us' pricing. None of this is confirmed on Vellum's own site, which currently shows only the unrelated consumer product's $50/mo Pro plan.", + "The cited third-party breakdown describes Pro at roughly $500/month with 5,000 prompt executions/day and 250 workflow executions/day; Enterprise tiers above that are custom pricing. None of this is confirmed on Vellum's own site, which currently shows only the unrelated consumer product's Pro packages starting at $30/month.", shortValue: '~$500/mo reported by a third party, unconfirmed by Vellum', confidence: 'unknown', sources: [ { url: 'https://www.zenml.io/blog/vellum-ai-pricing', label: 'Vellum AI Pricing: ZenML Blog', - asOf: '2026-07-04', + asOf: '2026-10-15', }, ], }, @@ -739,22 +759,27 @@ export const vellumProfile: CompetitorProfile = { { url: 'https://www.zenml.io/blog/vellum-ai-pricing', label: 'Vellum AI Pricing: ZenML Blog', - asOf: '2026-07-04', + asOf: '2026-10-15', }, ], }, byok: { value: - "Unconfirmed for the enterprise platform: Vellum's pricing page has been replaced by an unrelated consumer 'Personal Intelligence' product page, so BYOK availability can no longer be verified from vellum.ai/pricing.", + "Yes, per Vellum's platform docs: most provider models are enabled in a workspace by adding your own API key from that provider, and private models (OpenAI on Azure, OpenAI fine-tunes, Fireworks-hosted fine-tunes) can be connected. How this interacts with billing is not published", detail: - "The current pricing page describes Vellum-provided credits for the consumer product, not the enterprise platform's LLM-cost pass-through model; no bring-your-own-API-key policy is described for either product on this page.", - shortValue: 'Unverifiable after the pricing page pivoted to the consumer product', - confidence: 'unknown', + "The marketing pricing page now describes only the consumer 'Personal Intelligence' product, so this rests on the platform documentation, which carries no publication date.", + shortValue: 'Own provider keys per platform docs; billing effect unpublished', + confidence: 'estimated', sources: [ + { + url: 'https://docs.vellum.ai/product/prompts/custom-models', + label: 'Vellum Docs: Custom Models', + asOf: '2026-10-15', + }, { url: 'https://www.vellum.ai/pricing', label: 'Vellum Pricing (now shows the consumer product)', - asOf: '2026-07-08', + asOf: '2026-10-15', }, ], }, diff --git a/apps/sim/lib/compare/data/sim.ts b/apps/sim/lib/compare/data/sim.ts index e4fe00624d6..c4169ff26d3 100644 --- a/apps/sim/lib/compare/data/sim.ts +++ b/apps/sim/lib/compare/data/sim.ts @@ -33,17 +33,17 @@ export const simProfile: CompetitorProfile = { shortDescription: 'Chat builds and manages work across the workspace; in-editor Copilot edits a single workflow.', source: { - url: 'https://docs.sim.ai/copilot', - label: 'Sim Docs: Copilot', + url: 'https://docs.sim.ai/chat/workflows', + label: 'Sim Docs: Chat (build and edit workflows)', asOf: '2026-07-08', }, }, { title: 'Hybrid semantic + keyword knowledge base', description: - 'Built-in RAG with pgvector embeddings and a generated tsvector column for combined vector + full-text search, plus a token-based chunker with configurable chunk size/overlap and 11 supported file formats (csv, doc, docx, html, json, md, pdf, pptx, txt, xlsx, yaml).', + 'Built-in RAG with pgvector embeddings and a generated tsvector column for combined vector + full-text search, plus a token-based chunker with configurable chunk size/overlap and 15 supported file types (csv, doc, docx, htm, html, json, jsonl, md, pdf, pptx, txt, xls, xlsx, yaml, yml).', shortDescription: - 'Combines vector and full-text search with configurable chunking across 11 file formats.', + 'Combines vector and full-text search with configurable chunking across 15 file types.', source: { url: 'https://github.com/simstudioai/sim/blob/main/packages/db/schema.ts', label: 'Sim codebase: KB schema + file-parsers', @@ -80,7 +80,7 @@ export const simProfile: CompetitorProfile = { shortDescription: 'Pause a run for human approval and resume later via a durable snapshot link.', source: { - url: 'https://docs.sim.ai/blocks/human-in-the-loop', + url: 'https://docs.sim.ai/workflows/blocks/human-in-the-loop', label: 'Sim Docs: Human in the Loop Block', asOf: '2026-07-02', }, @@ -88,8 +88,8 @@ export const simProfile: CompetitorProfile = { { title: 'Self-hostable under Apache 2.0', description: - 'Fully open source (Apache 2.0), with Docker Compose files and a Helm chart for Kubernetes deployment, alongside a managed cloud-hosted option.', - shortDescription: 'Fully open source with Docker Compose and Helm deployment options.', + 'Open-source core under Apache 2.0, with Docker Compose files and a Helm chart for Kubernetes deployment, alongside a managed cloud-hosted option. Enterprise features (SSO, SCIM, audit logs, access control, workspace forks) are separately licensed and require an Enterprise subscription for production use.', + shortDescription: 'Apache 2.0 core with Docker Compose and Helm deployment options.', source: { url: 'https://docs.sim.ai/introduction', label: 'Sim Docs: Introduction (FAQ - Open Source License)', @@ -166,12 +166,12 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/self-hosting/docker', + url: 'https://docs.sim.ai/platform/self-hosting/docker', label: 'Sim Docs: Self-Hosting with Docker', asOf: '2026-07-02', }, { - url: 'https://docs.sim.ai/self-hosting/kubernetes', + url: 'https://docs.sim.ai/platform/self-hosting/kubernetes', label: 'Sim Docs: Kubernetes (Helm) self-hosting', asOf: '2026-07-02', }, @@ -211,8 +211,9 @@ export const simProfile: CompetitorProfile = { ], }, license: { - value: 'Apache License 2.0', - shortValue: 'Apache 2.0', + value: + 'Apache License 2.0 for the core; enterprise features are under the separate Sim Enterprise License, which requires an Enterprise subscription for production use', + shortValue: 'Apache 2.0 core; enterprise features separately licensed', confidence: 'verified', sources: [ { @@ -366,9 +367,9 @@ export const simProfile: CompetitorProfile = { aiCapabilities: { multiLlmSupport: { value: - '21 provider integrations (OpenAI, Anthropic, Google/Gemini, Azure OpenAI, Azure Anthropic, Groq, Cerebras, Mistral, xAI, Bedrock, Vertex, Ollama, OpenRouter, and more), with OpenRouter, LiteLLM, vLLM, and Ollama resolving models dynamically at runtime rather than from a fixed list, so effective model reach extends well beyond the 21 named providers', - detail: 'apps/sim/providers/models.ts defines 21 provider entries.', - shortValue: '21 providers plus dynamic-resolution aggregators (OpenRouter, LiteLLM, etc.)', + '28 provider integrations (OpenAI, Anthropic, Google/Gemini, Azure OpenAI, Azure Anthropic, Vertex, Bedrock, xAI, Mistral, DeepSeek, Groq, Cerebras, Fireworks, Together, Ollama, OpenRouter, and more), with OpenRouter, LiteLLM, vLLM, and Ollama resolving models dynamically at runtime rather than from a fixed list, so effective model reach extends well beyond the 28 named providers', + detail: 'apps/sim/providers/models.ts defines 28 provider entries.', + shortValue: '28 providers plus dynamic-resolution aggregators (OpenRouter, LiteLLM, etc.)', confidence: 'verified', sources: [ { @@ -398,15 +399,15 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/copilot', - label: 'Sim Docs: Copilot', + url: 'https://docs.sim.ai/chat/workflows', + label: 'Sim Docs: Chat (build and edit workflows)', asOf: '2026-07-02', }, ], }, knowledgeBaseRag: { - value: `Yes: hybrid vector (pgvector) plus full-text (tsvector) search knowledge base, 11 supported file formats (csv, doc, docx, html, json, md, pdf, pptx, txt, xlsx, yaml), configurable chunking, plus ${CONNECTOR_COUNT_LABEL} connectors that continuously sync external sources (Google Drive, Confluence, Slack, Gmail, GitHub, HubSpot, Linear, Jira, and more) into the knowledge base rather than a one-shot upload`, - shortValue: `Hybrid vector + full-text search, 11 file formats, ${CONNECTOR_COUNT_LABEL} connectors`, + value: `Yes: hybrid vector (pgvector) plus full-text (tsvector) search knowledge base, 15 supported file types (csv, doc, docx, htm, html, json, jsonl, md, pdf, pptx, txt, xls, xlsx, yaml, yml), configurable chunking, plus ${CONNECTOR_COUNT_LABEL} connectors that continuously sync external sources (Google Drive, Confluence, Slack, Gmail, GitHub, HubSpot, Linear, Jira, and more) into the knowledge base rather than a one-shot upload`, + shortValue: `Hybrid vector + full-text search, 15 file types, ${CONNECTOR_COUNT_LABEL} connectors`, confidence: 'verified', sources: [ { @@ -433,7 +434,7 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/mcp', + url: 'https://docs.sim.ai/agents/mcp', label: 'Sim Docs: Using MCP Tools', asOf: '2026-07-02', }, @@ -453,12 +454,12 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/blocks/evaluator', + url: 'https://docs.sim.ai/workflows/blocks/evaluator', label: 'Sim Docs: Evaluator Block', asOf: '2026-07-02', }, { - url: 'https://docs.sim.ai/blocks/guardrails', + url: 'https://docs.sim.ai/workflows/blocks/guardrails', label: 'Sim Docs: Guardrails Block', asOf: '2026-07-02', }, @@ -471,7 +472,7 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/blocks/human-in-the-loop', + url: 'https://docs.sim.ai/workflows/blocks/human-in-the-loop', label: 'Sim Docs: Human in the Loop Block', asOf: '2026-07-02', }, @@ -549,7 +550,7 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/skills', + url: 'https://docs.sim.ai/agents/skills', label: 'Sim Docs: Agent skills', asOf: '2026-07-02', }, @@ -642,7 +643,7 @@ export const simProfile: CompetitorProfile = { asOf: '2026-10-13', }, { - url: 'https://docs.sim.ai/tools', + url: 'https://docs.sim.ai/integrations', label: 'Sim Docs: Integrations / Tools Overview', asOf: '2026-07-02', }, @@ -659,7 +660,7 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/triggers', + url: 'https://docs.sim.ai/workflows#triggers', label: 'Sim Docs: Triggers overview', asOf: '2026-07-02', }, @@ -721,7 +722,7 @@ export const simProfile: CompetitorProfile = { asOf: '2026-07-08', }, { - url: 'https://docs.sim.ai/execution/api', + url: 'https://docs.sim.ai/api-reference/getting-started', label: 'Sim Docs: External API', asOf: '2026-07-08', }, @@ -744,7 +745,7 @@ export const simProfile: CompetitorProfile = { asOf: '2026-07-04', }, { - url: 'https://docs.sim.ai/mcp', + url: 'https://docs.sim.ai/agents/mcp', label: 'Sim Docs: Using MCP Tools', asOf: '2026-07-02', }, @@ -772,14 +773,14 @@ export const simProfile: CompetitorProfile = { pricing: { pricingModel: { value: - 'Credit-based usage billing (Stripe), with bring-your-own-key exemption from metered caps', - shortValue: 'Credit-based billing, BYOK exempt from caps', + "Credit-based usage billing (Stripe); bring-your-own-key calls skip Sim's model and tool usage charge, while the base per-run charge still applies", + shortValue: 'Credit-based billing; BYOK skips model usage charges', confidence: 'verified', sources: [ { url: `${SITE_URL}/pricing`, label: 'Sim Pricing', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -791,7 +792,7 @@ export const simProfile: CompetitorProfile = { { url: `${SITE_URL}/pricing`, label: 'Sim Pricing', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -804,35 +805,35 @@ export const simProfile: CompetitorProfile = { { url: `${SITE_URL}/pricing`, label: 'Sim Pricing', - asOf: '2026-08-26', + asOf: '2026-10-15', }, { url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/lib/billing/constants.ts', label: 'Sim codebase: DEFAULT_FREE_CREDITS', - asOf: '2026-08-26', + asOf: '2026-10-15', }, ], }, byok: { value: - 'Yes: bring-your-own-key support exempts usage from metered credit caps, and multiple keys stored for the same provider are automatically round-robin rotated, with automatic fallback past any key that fails to decrypt', - shortValue: 'BYOK exempts credit caps; multi-key round-robin rotation', + "Yes: bring-your-own-key provider calls skip Sim's model and tool usage charge (the base per-run charge still applies); workspace keys work on any plan, organization keys need a team or Enterprise plan, and multiple keys stored for the same provider are round-robin rotated, skipping any key that fails to decrypt", + shortValue: 'BYOK skips model usage charges; multi-key round-robin rotation', confidence: 'verified', sources: [ { url: 'https://docs.sim.ai/platform/costs#bring-your-own-key-byok', label: 'Sim Docs: Bring Your Own Key (BYOK)', - asOf: '2026-07-02', + asOf: '2026-10-15', }, { url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/lib/billing/calculations/usage-monitor.ts', label: 'Sim codebase: BYOK usage-monitor logic', - asOf: '2026-07-02', + asOf: '2026-10-15', }, { url: 'https://github.com/simstudioai/sim/blob/main/apps/sim/lib/api-key/byok.ts', label: 'Sim codebase: BYOK key rotation', - asOf: '2026-07-02', + asOf: '2026-10-15', }, ], }, @@ -863,7 +864,7 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/self-hosting/docker', + url: 'https://docs.sim.ai/platform/self-hosting/docker', label: 'Sim Docs: Self-Hosting with Docker', asOf: '2026-07-02', }, @@ -946,7 +947,7 @@ export const simProfile: CompetitorProfile = { asOf: '2026-07-02', }, { - url: 'https://docs.sim.ai/permissions/roles-and-permissions', + url: 'https://docs.sim.ai/platform/permissions', label: 'Sim Docs: Roles and Permissions', asOf: '2026-07-02', }, @@ -961,7 +962,7 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/permissions/roles-and-permissions', + url: 'https://docs.sim.ai/platform/permissions', label: 'Sim Docs: Roles and Permissions', asOf: '2026-07-02', }, @@ -1005,7 +1006,7 @@ export const simProfile: CompetitorProfile = { confidence: 'verified', sources: [ { - url: 'https://docs.sim.ai/blocks/guardrails', + url: 'https://docs.sim.ai/workflows/blocks/guardrails', label: 'Sim Docs: Guardrails Block', asOf: '2026-07-02', }, @@ -1075,7 +1076,7 @@ export const simProfile: CompetitorProfile = { asOf: '2026-07-02', }, { - url: 'https://docs.sim.ai/execution/logging', + url: 'https://docs.sim.ai/logs-debugging/logging', label: 'Sim Docs: Logging', asOf: '2026-07-02', }, @@ -1201,7 +1202,7 @@ export const simProfile: CompetitorProfile = { asOf: '2026-07-02', }, { - url: 'https://docs.sim.ai/triggers', + url: 'https://docs.sim.ai/workflows#triggers', label: 'Sim Docs: Triggers', asOf: '2026-07-02', }, diff --git a/apps/sim/lib/compare/data/types.ts b/apps/sim/lib/compare/data/types.ts index 88f9db4d59a..57827d88881 100644 --- a/apps/sim/lib/compare/data/types.ts +++ b/apps/sim/lib/compare/data/types.ts @@ -232,6 +232,12 @@ export interface CompetitorProfile { * decides it. 3-5 sentences. */ betterThanAnswer?: Prose + /** + * Hand-written FAQs answering this competitor's tracked buyer prompts, shown + * ahead of the generated FAQ set. Answer-first, and every answer names both + * products so it can be quoted on its own. + */ + faqs?: Array<{ question: string; answer: string }> /** * One-sentence lead-in per comparison-table section, stating what that * section covers so the section is quotable without the rest of the page. diff --git a/apps/sim/lib/content/mdx.tsx b/apps/sim/lib/content/mdx.tsx index 5babec975e6..8e15fdb4233 100644 --- a/apps/sim/lib/content/mdx.tsx +++ b/apps/sim/lib/content/mdx.tsx @@ -114,8 +114,8 @@ export const mdxComponents: MDXRemoteProps['components'] = { className={cn('font-semibold text-[var(--text-primary)]', props.className)} /> ), - em: (props: any) => ( - + em: ({ className, ...props }: ComponentPropsWithoutRef<'em'>) => ( + ), a: (props: any) => { const isAnchorLink = props.className?.includes('anchor') diff --git a/apps/sim/lib/core/application/content-delivery.test.ts b/apps/sim/lib/core/application/content-delivery.test.ts index 5cc74901232..037f25c7057 100644 --- a/apps/sim/lib/core/application/content-delivery.test.ts +++ b/apps/sim/lib/core/application/content-delivery.test.ts @@ -12,6 +12,7 @@ import { ContentDeliveryUnobservedError, ContentRefusedError, type ContentSource, + countWithheldContent, mustClassifyContent, noteContentDeliveryOperation, observeContentDelivery, @@ -509,3 +510,49 @@ describe('observeContentOutsideModel', () => { expect(batches).toEqual([]) }) }) + +describe('countWithheldContent', () => { + it('counts every refused item by kind across the requests and use cases it ran', async () => { + const { gate } = recordingGate() + const row = (id: string): ContentSource => ({ ...source(id, 'unknown'), kind: 'table-row' }) + const { result, withheld } = await observeContentDelivery(gate, () => + countWithheldContent(async () => { + await trackContentDelivery(() => admitContent(CHAT, [row('a'), row('b'), source('clean')])) + const admission = await admitContent(CHAT, [source('file', 'unknown')]) + return admission.refused.length + }) + ) + expect(result).toBe(1) + expect([...withheld].map(([kind, reasons]) => [kind, [...reasons]])).toEqual([ + ['table-row', [['unknown', 2]]], + ['workspace-file', [['unknown', 1]]], + ]) + }) + + it('keeps the reason a gate gave for each refusal', async () => { + const gate: ContentDeliveryGate = { + async admit(sources) { + return sources.map((item) => + item.id === 'raw' ? 'opaque' : item.id === 'later' ? 'registry' : true + ) + }, + } + const { withheld } = await observeContentDelivery(gate, () => + countWithheldContent(() => + admitContent(CHAT, [source('raw'), source('later'), source('clean')]) + ) + ) + expect([...(withheld.get('workspace-file') ?? [])]).toEqual([ + ['opaque', 1], + ['registry', 1], + ]) + }) + + it('counts nothing for a caller that never classifies', async () => { + const { gate } = recordingGate() + const { withheld } = await observeContentDelivery(gate, () => + countWithheldContent(() => admitContent(EXECUTOR, [source('file', 'unknown')])) + ) + expect(withheld.size).toBe(0) + }) +}) diff --git a/apps/sim/lib/core/application/content-delivery.ts b/apps/sim/lib/core/application/content-delivery.ts index 1d6a1bdec59..bc5f3d52259 100644 --- a/apps/sim/lib/core/application/content-delivery.ts +++ b/apps/sim/lib/core/application/content-delivery.ts @@ -37,12 +37,28 @@ export interface ContentSource { view: ContentView } +/** + * Why a gate refused an item. `unknown`: its secret provenance is unknown, so no read returns it. + * `opaque`: it holds recorded secrets in a view no redaction reaches (raw bytes, an excerpt), while + * its full text can be redacted. `registry`, `import`: its provenance could not be checked now. + */ +export type ContentRefusalReason = 'unknown' | 'opaque' | 'registry' | 'import' + +/** `true` admits; a reason refuses. A bare `false` refuses for an unstated reason, read as `unknown`. */ +export type ContentVerdict = boolean | ContentRefusalReason + +/** What a gate refused inside a {@link countWithheldContent} call, by kind and then reason. */ +export type WithheldContent = ReadonlyMap< + ContentSourceKind, + ReadonlyMap +> + /** * Decides, per item, whether content may leave through the surface that installed it. A gate never * throws for a refusal: refusing one item must not cost the call the items it admits. */ export interface ContentDeliveryGate { - admit(sources: readonly ContentSource[]): Promise + admit(sources: readonly ContentSource[]): Promise /** * Redacts text before cutting, or returns `undefined` when it cannot be shown. The current * operation's bounded evidence lets a surface create a redactor only when a cut needs one. @@ -73,6 +89,8 @@ interface ContentLedger { accounted: boolean /** Bounded aggregate of admitted provenance, for a surface that persists the delivered bytes. */ admitted: ContentProvenance[] + /** Only on a {@link countWithheldContent} ledger: the items refused inside it. */ + withheld?: Map> /** A committed write's returned item was refused; the operation answers with a refusal. */ withheldAfterWrite?: ContentSourceKind /** The gate itself failed while this ledger was open, whatever the route then answered. */ @@ -205,7 +223,7 @@ export function mustClassifyContent(principal: Principal): boolean { export class ContentAdmission { private readonly verdicts: ReadonlyMap - constructor(sources: readonly ContentSource[], verdicts: readonly boolean[]) { + constructor(sources: readonly ContentSource[], verdicts: readonly ContentVerdict[]) { this.verdicts = new Map(sources.map((source, index) => [source, verdicts[index] === true])) } @@ -291,7 +309,7 @@ export async function admitContent( const gate = gates.getStore() if (!gate) throw new ContentDeliveryUnobservedError() const ledger = ledgers.getStore() - let verdicts: readonly boolean[] + let verdicts: readonly ContentVerdict[] try { verdicts = await gate.admit(sources) } catch (error) { @@ -303,10 +321,22 @@ export async function admitContent( } markAccounted() if (ledger) { + for (let current: ContentLedger | undefined = ledger; current; current = current.parent) { + const { withheld } = current + if (!withheld) continue + sources.forEach(({ kind }, index) => { + const verdict = verdicts[index] + if (verdict === true) return + const reason = typeof verdict === 'string' ? verdict : 'unknown' + const reasons = withheld.get(kind) ?? new Map() + reasons.set(reason, (reasons.get(reason) ?? 0) + 1) + withheld.set(kind, reasons) + }) + } const batch = mergeContentProvenance( (function* () { for (let index = 0; index < sources.length; index++) { - if (verdicts[index]) yield sources[index].provenance + if (verdicts[index] === true) yield sources[index].provenance } })() ) @@ -459,6 +489,26 @@ export async function trackContentDelivery( } } +/** + * Runs `execute` and counts, by kind, the items a gate refused inside it, across every request and + * use case it ran. A refused item is left out of the result it was read in, so a surface reporting + * to Chat uses this to say what is missing rather than let it read as absent. + */ +export async function countWithheldContent( + execute: () => Promise +): Promise<{ result: T; withheld: WithheldContent }> { + const withheld = new Map>() + const ledger: ContentLedger = { + parent: ledgers.getStore(), + sourcedOperationAdmitted: false, + accounted: false, + admitted: [], + withheld, + } + const result = await ledgers.run(ledger, execute) + return { result, withheld } +} + /** Route admission records that Chat was admitted to an operation, in the request's ledger. */ export function noteContentDeliveryOperation( operation: ApplicationOperation, diff --git a/apps/sim/lib/core/application/index.ts b/apps/sim/lib/core/application/index.ts index 5b91b1fd8f2..47b0b126ef3 100644 --- a/apps/sim/lib/core/application/index.ts +++ b/apps/sim/lib/core/application/index.ts @@ -70,10 +70,13 @@ export { ContentDeliveryUnobservedError, ContentGateFailure, type ContentProvenance, + type ContentRefusalReason, ContentRefusedError, type ContentSource, type ContentSourceKind, + type ContentVerdict, type ContentView, + countWithheldContent, isCopilotPrincipal, mustClassifyContent, noteContentDeliveryOperation, @@ -83,4 +86,5 @@ export { requireAdmittedContent, runDeclaredContentOperation, trackContentDelivery, + type WithheldContent, } from './content-delivery' diff --git a/apps/sim/lib/core/application/organization-authorization.ts b/apps/sim/lib/core/application/organization-authorization.ts index d3469969343..f65538cd71e 100644 --- a/apps/sim/lib/core/application/organization-authorization.ts +++ b/apps/sim/lib/core/application/organization-authorization.ts @@ -28,6 +28,13 @@ export class OrganizationMembershipNotFoundError extends OrchestrationError { } } +/** The caller's organization delegation is expired or out of scope, not the member's access. */ +export class OrganizationDelegationAuthorizationError extends OrchestrationError { + constructor() { + super('forbidden', 'Organization delegation is no longer valid') + } +} + export interface OrganizationAuthorizationContext { organizationId: string } @@ -129,7 +136,7 @@ export async function authorizeOrganizationOperation( ? !principal.resourceScope.chatId : !principal.resourceScope.installationId || !principal.resourceScope.eventId) ) { - throw new OrchestrationError('forbidden', 'Organization delegation is no longer valid') + throw new OrganizationDelegationAuthorizationError() } return requireOrganizationSubjectMembership( principal.subjectUserId, diff --git a/apps/sim/lib/core/application/workspace-authorization.ts b/apps/sim/lib/core/application/workspace-authorization.ts index 721c59d7007..67de9320502 100644 --- a/apps/sim/lib/core/application/workspace-authorization.ts +++ b/apps/sim/lib/core/application/workspace-authorization.ts @@ -191,7 +191,10 @@ export function requireAllowedWorkspacePrincipal( } } -function requirePermission(permission: PermissionType | null, required: PermissionType): void { +function requirePermission( + permission: PermissionType | null, + required: PermissionType +): asserts permission is PermissionType { if (permission === null) { throw new NoWorkspaceAccessError() } @@ -324,7 +327,7 @@ export async function requireCurrentHumanRole -): Promise { +): Promise { const permission = await resolveEffectiveWorkspacePermission( userId, context.workspaceId, @@ -333,6 +336,7 @@ export async function requireCurrentHumanRole { + const { memoryCleanupOutboxHandlers } = await import( + '@/lib/mothership/memories/cleanup-outbox' + ) + return memoryCleanupOutboxHandlers + }, + }, { events: [SLACK_SEARCH_TURN_EVENT], load: async () => { diff --git a/apps/sim/lib/credential-groups/__integration__/slack-shared-member-accounts.integration.ts b/apps/sim/lib/credential-groups/__integration__/slack-shared-member-accounts.integration.ts index de45d377b5c..4f978575962 100644 --- a/apps/sim/lib/credential-groups/__integration__/slack-shared-member-accounts.integration.ts +++ b/apps/sim/lib/credential-groups/__integration__/slack-shared-member-accounts.integration.ts @@ -47,11 +47,14 @@ import { encryptManagedOAuthTokenSet } from '@/lib/credentials/managed-oauth' import type { DbOrTx, DbTransaction } from '@/lib/db/types' import { approveSearchIntegration } from '@/lib/knowledge/application/search-integrations' import { + configureSlackSearchInstallation, listSlackSearchInstallations, removeSlackSearchInstallation, } from '@/lib/knowledge/application/slack-search/installations' import { revokeSlackSearchAccess } from '@/lib/knowledge/application/slack-search/lifecycle' +import { SLACK_CUSTOM_BOT_PROVIDER_ID, SLACK_CUSTOM_BOT_SECRET_TYPE } from '@/lib/oauth/types' import { defaultLiveSearchPolicy } from '@/lib/sim-search/live/policy-schema' +import { SLACK_SHARED_SEARCH_BOT_SCOPES } from '@/lib/slack-search/constants' import { getSharedSlackSearchAppConfiguration } from '@/lib/slack-search/shared-app-env' vi.mock('@/lib/core/config/env-flags', async (importOriginal) => ({ @@ -156,8 +159,8 @@ describe('Slack member accounts on the shared app without a Search bot', () => { input: { state, code: 'fixture-code' }, }) - async function installBot(appId: string, teamId: string, executor: DbOrTx = db) { - const secret = (await encryptSecret('fixture-bot')).encrypted + async function upsertSlackApp(appId: string, executor: DbOrTx = db) { + const secret = (await encryptSecret('fixture-app')).encrypted await executor .insert(slackApp) .values( @@ -174,6 +177,11 @@ describe('Slack member accounts on the shared app without a Search bot', () => { } ) .onConflictDoNothing() + } + + async function installBot(appId: string, teamId: string, executor: DbOrTx = db) { + await upsertSlackApp(appId, executor) + const secret = (await encryptSecret('fixture-bot')).encrypted const botId = generateId() await executor.insert(credential).values({ id: botId, @@ -281,6 +289,99 @@ describe('Slack member accounts on the shared app without a Search bot', () => { restoreSlackHttp = () => spy.mockRestore() } + /** Answers bot verification as a bot of `appId` installed in `teamId`. */ + function provideSlackBot(appId: string, teamId: string) { + restoreSlackHttp?.() + const spy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const url = new URL(String(input)) + switch (`${url.origin}${url.pathname}`) { + case 'https://slack.com/api/auth.test': + return Response.json( + { ok: true, team_id: teamId, team: 'Fixture team', user_id: 'UBOT', bot_id: 'BBOT' }, + { headers: { 'x-oauth-scopes': SLACK_SHARED_SEARCH_BOT_SCOPES.join(',') } } + ) + case 'https://slack.com/api/bots.info': + return Response.json({ ok: true, bot: { id: 'BBOT', app_id: appId } }) + default: + throw new Error(`Unexpected Slack fixture request: ${url}`) + } + }) + restoreSlackHttp = () => spy.mockRestore() + } + + async function connectMembersOnSharedApp() { + const groupId = (await approveSlack()).memberAccounts?.groupId + if (!groupId) throw new Error('Approval did not provision organization accounts') + const pending = new URL((await startSetup(groupId)).authorizationUrl) + provideSlackConsent(MEMBER_TEAM) + await completeSetup(pending.searchParams.get('state') ?? '') + } + + /** A bot credential left without a Search installation, as after removing its binding. */ + async function insertLeftoverBot(appId: string, teamId: string) { + await upsertSlackApp(appId) + const { encrypted } = await encryptSecret( + JSON.stringify({ + type: SLACK_CUSTOM_BOT_SECRET_TYPE, + botToken: 'xoxb-fixture', + teamId, + botUserId: 'UBOT', + teamName: 'Fixture team', + }) + ) + const credentialId = generateId() + await db.insert(credential).values({ + id: credentialId, + organizationId: ids.organization, + type: 'service_account', + providerId: SLACK_CUSTOM_BOT_PROVIDER_ID, + displayName: 'Leftover Slack bot', + createdBy: ids.owner, + encryptedServiceAccountKey: encrypted, + slackAppId: appId, + }) + return credentialId + } + + const enableBot = (credentialId: string) => + configureSlackSearchInstallation.execute({ + principal: principal(), + input: { organizationId: ids.organization, credentialId, enabled: true }, + }) + + const installations = () => + db + .select() + .from(slackSearchInstallation) + .where(eq(slackSearchInstallation.organizationId, ids.organization)) + + it('refuses to bind a leftover bot to a different app or workspace than members', async () => { + await connectMembersOnSharedApp() + const credentialId = await insertLeftoverBot(CUSTOM_APP_ID, OTHER_TEAM) + provideSlackBot(CUSTOM_APP_ID, OTHER_TEAM) + await expect(enableBot(credentialId)).rejects.toMatchObject({ + code: 'conflict', + message: 'Install the same Slack app and workspace connected by your members', + }) + expect(await installations()).toEqual([]) + }) + + it('binds a leftover shared-app bot in the members workspace', async () => { + await connectMembersOnSharedApp() + const credentialId = await insertLeftoverBot(SHARED_APP_ID, MEMBER_TEAM) + provideSlackBot(SHARED_APP_ID, MEMBER_TEAM) + const created = await enableBot(credentialId) + expect(await installations()).toEqual([ + expect.objectContaining({ + id: created.id, + credentialId, + appId: SHARED_APP_ID, + teamId: MEMBER_TEAM, + enabled: true, + }), + ]) + }) + it('approves Slack, verifies the workspace from the admin grant, and resolves member sign-in', async () => { const approval = await approveSlack() expect(approval).toMatchObject({ approved: true }) diff --git a/apps/sim/lib/credential-groups/organization-slack-app.ts b/apps/sim/lib/credential-groups/organization-slack-app.ts index b9c0124af29..a66cf54fc73 100644 --- a/apps/sim/lib/credential-groups/organization-slack-app.ts +++ b/apps/sim/lib/credential-groups/organization-slack-app.ts @@ -5,6 +5,7 @@ import { OrchestrationError } from '@/lib/core/orchestration/types' import { decryptCredentialGroupProviderConfiguration, encryptCredentialGroupProviderConfiguration, + type SlackCredentialGroupConfiguration, } from '@/lib/credential-groups/provider-configuration' import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks' import type { DbOrTx, DbTransaction } from '@/lib/db/types' @@ -49,6 +50,57 @@ export async function loadOrganizationSlackMemberApps( return configurations.filter((row) => row.configuration.slack !== undefined) } +/** + * Resolves the one Slack app and workspace the organization's members connect through, with the + * union of their scopes. Fails when member configurations reference more than one app. + */ +export async function resolveOrganizationSlackMemberApp( + organizationId: string, + executor: DbOrTx = db +) { + const rows = await loadOrganizationSlackMemberApps(organizationId, executor) + const configurations = rows.flatMap((row) => + row.configuration.slack ? [row.configuration.slack] : [] + ) + const first = configurations[0] + if ( + first && + configurations.some( + (configuration) => + configuration.appId !== first.appId || configuration.teamId !== first.teamId + ) + ) + throw new OrchestrationError( + 'conflict', + 'Organization Slack member accounts reference multiple apps. Consolidate them before configuring Search.' + ) + return { + app: first ? { appId: first.appId, teamId: first.teamId } : null, + scopes: [...new Set(configurations.flatMap((configuration) => configuration.scopes))], + } +} + +type SlackAppIdentity = Pick + +/** + * Refuses a Search bot outside the members' Slack workspace, or on a different app unless the + * bot runs on the shared Sim Search app. + */ +export function requireSearchBotMatchesMemberApp( + memberApp: SlackAppIdentity | null | undefined, + bot: SlackAppIdentity, + sharedBot: boolean +) { + if ( + memberApp && + ((!sharedBot && memberApp.appId !== bot.appId) || memberApp.teamId !== bot.teamId) + ) + throw new OrchestrationError( + 'conflict', + 'Install the same Slack app and workspace connected by your members' + ) +} + /** Replaces legacy duplicated secrets without changing member grants or their scope policies. */ export async function adoptOrganizationSlackMemberApp( executor: DbOrTx, diff --git a/apps/sim/lib/credential-groups/provider-configuration.ts b/apps/sim/lib/credential-groups/provider-configuration.ts index 6ddcce5ce02..62e71efe89c 100644 --- a/apps/sim/lib/credential-groups/provider-configuration.ts +++ b/apps/sim/lib/credential-groups/provider-configuration.ts @@ -159,7 +159,8 @@ async function resolveSlackConfiguration( .limit(1) if (!app) throw new Error('Organization Slack app configuration is missing') /** Member grants are verified against the app and team directly; the Search bot is optional. */ - if (app.kind === 'shared') await requireSlackSearchAppAvailable(app.id, params.organizationId) + if (app.kind === 'shared') + await requireSlackSearchAppAvailable(app.id, params.organizationId, params.executor) const resolved = await resolveSlackAppCredentials(app) return { appId: app.id, diff --git a/apps/sim/lib/credential-groups/slack-managed-users.ts b/apps/sim/lib/credential-groups/slack-managed-users.ts index b9649f75429..4aefdb93f45 100644 --- a/apps/sim/lib/credential-groups/slack-managed-users.ts +++ b/apps/sim/lib/credential-groups/slack-managed-users.ts @@ -918,7 +918,7 @@ export async function exchangeAndConfigureSlackManagedUsers(params: { 'This Slack app belongs to another installation owner' ) if (app?.kind === 'shared') - await requireSlackSearchAppAvailable(app.id, params.attempt.organizationId) + await requireSlackSearchAppAvailable(app.id, params.attempt.organizationId, tx) const resolved = app ? await resolveSlackAppCredentials(app) : null if ( !resolved || diff --git a/apps/sim/lib/data-drains/sources/copilot-chats.ts b/apps/sim/lib/data-drains/sources/copilot-chats.ts index 636bfaedd9e..b79062b33d2 100644 --- a/apps/sim/lib/data-drains/sources/copilot-chats.ts +++ b/apps/sim/lib/data-drains/sources/copilot-chats.ts @@ -1,5 +1,7 @@ import { copilotChats, copilotMessages } from '@sim/db/schema' +import { isRecordLike, omit } from '@sim/utils/object' import { and, asc, eq, inArray, isNull, or, sql } from 'drizzle-orm' +import { persistedContentBlockSchema } from '@/lib/api/contracts/copilot-messages' import { assertKnownSizeWithinLimit } from '@/lib/core/utils/stream-limits' import { DATA_DRAIN_LIMITS } from '@/lib/data-drains/limits' import { @@ -11,6 +13,7 @@ import { } from '@/lib/data-drains/sources/cursor' import { readBoundedSourcePage, workspaceInOrganization } from '@/lib/data-drains/sources/helpers' import type { Cursor, DrainSource, SourcePageInput } from '@/lib/data-drains/types' +import { stripToolResultOutput } from '@/lib/mothership/chat/persisted-message' /** * The transcript no longer lives on `copilot_chats.messages` — it is assembled @@ -141,6 +144,47 @@ async function* pages(input: SourcePageInput): AsyncIterable { } } +/** A message written before content blocks keeps its tool calls in a top-level `toolCalls` list. */ +function projectLegacyToolCall(call: unknown): unknown { + if (!isRecordLike(call) || call.name !== 'remember') return call + return { + name: 'remember', + ...(typeof call.id === 'string' ? { id: call.id } : {}), + ...(typeof call.status === 'string' ? { status: call.status } : {}), + } +} + +/** Tool payloads can contain private notes even when their mutation failed or targeted a shared note. */ +function projectExportMessage(message: unknown): unknown { + if (!isRecordLike(message)) return message + const projected = Array.isArray(message.toolCalls) + ? { ...message, toolCalls: message.toolCalls.map(projectLegacyToolCall) } + : message + if (!Array.isArray(projected.contentBlocks)) return projected + return { + ...projected, + contentBlocks: projected.contentBlocks.map((block: unknown) => { + if (!isRecordLike(block) || !isRecordLike(block.toolCall)) return block + if (block.toolCall.name !== 'remember') { + if (!isRecordLike(block.toolCall.result) || !('output' in block.toolCall.result)) + return block + const parsed = persistedContentBlockSchema.safeParse(block) + if (!parsed.success) return { ...block, toolCall: omit(block.toolCall, ['result']) } + const projected = stripToolResultOutput({ contentBlocks: [parsed.data] }).contentBlocks[0] + return { ...block, toolCall: { ...block.toolCall, result: projected.toolCall?.result } } + } + return { + type: 'tool', + toolCall: { + name: 'remember', + ...(typeof block.toolCall.id === 'string' ? { id: block.toolCall.id } : {}), + ...(typeof block.toolCall.state === 'string' ? { state: block.toolCall.state } : {}), + }, + } + }), + } +} + export const copilotChatsSource: DrainSource = { type: 'copilot_chats', displayName: 'Chats', @@ -153,7 +197,7 @@ export const copilotChatsSource: DrainSource = { workspaceId: row.workspaceId, type: row.type, title: row.title, - messages: row.messages, + messages: row.messages.map(projectExportMessage), model: row.model, conversationId: row.conversationId, previewYaml: row.previewYaml, diff --git a/apps/sim/lib/data-drains/sources/coverage.integration.ts b/apps/sim/lib/data-drains/sources/coverage.integration.ts index e22770f9cd5..45b3bdb826c 100644 --- a/apps/sim/lib/data-drains/sources/coverage.integration.ts +++ b/apps/sim/lib/data-drains/sources/coverage.integration.ts @@ -113,6 +113,98 @@ describe('Drain source coverage and payload safety in PostgreSQL', () => { expect(runs).toEqual([runId]) }) + it('omits memory payloads from exports without changing owner history or visible replies', async () => { + const messageId = generateId() + const content = { + role: 'assistant', + content: 'Visible reply', + toolCalls: [ + { + id: 'legacy', + name: 'remember', + status: 'success', + params: { content: 'hidden-memory-legacy-args' }, + result: 'hidden-memory-legacy-result', + error: 'hidden-memory-legacy-error', + }, + { + id: 'legacy-read', + name: 'read', + status: 'success', + params: { path: 'visible-legacy-path' }, + }, + ], + contentBlocks: [ + { type: 'text', content: 'Visible reply' }, + { + type: 'tool', + toolCall: { + id: 'discovery', + name: 'list_workspaces', + state: 'success', + result: { + success: true, + output: { + stdout: JSON.stringify({ storedNotes: [{ content: 'hidden-memory-discovery' }] }), + }, + }, + }, + }, + ...['private', 'shared', 'unknown'].map((visibility) => ({ + type: 'tool', + content: 'hidden-memory-content', + error: 'hidden-memory-error', + toolCall: { + id: visibility, + name: 'remember', + state: 'success', + params: { visibility, content: 'hidden-memory-args' }, + result: { success: true, output: 'hidden-memory-result' }, + activityDescription: 'hidden-memory-activity', + display: { title: 'hidden-memory-title' }, + }, + })), + { + type: 'tool', + toolCall: { + id: 'other', + name: 'read', + state: 'success', + params: { path: 'visible-path' }, + }, + }, + ], + } + await runtime.db.insert(runtime.schema.copilotMessages).values({ + id: messageId, + messageId, + chatId, + role: 'assistant', + content, + createdAt: old, + }) + try { + const exported: unknown[] = [] + for await (const rows of runtime.copilotChatsSource.pages(input())) { + exported.push(...rows.map((row) => runtime.copilotChatsSource.serialize(row))) + } + const wire = JSON.stringify(exported) + expect(wire).not.toContain('hidden-memory') + expect(wire).toContain('Visible reply') + expect(wire).toContain('visible-path') + expect(wire).toContain('visible-legacy-path') + const [stored] = await runtime.db + .select({ content: runtime.schema.copilotMessages.content }) + .from(runtime.schema.copilotMessages) + .where(runtime.eq(runtime.schema.copilotMessages.id, messageId)) + expect(stored.content).toEqual(content) + } finally { + await runtime.db + .delete(runtime.schema.copilotMessages) + .where(runtime.eq(runtime.schema.copilotMessages.id, messageId)) + } + }) + it('exports correlation metadata without internal controller tokens or billing receipts', async () => { const [row] = await runtime.db .select() diff --git a/apps/sim/lib/desktop/application/executor.integration.ts b/apps/sim/lib/desktop/application/executor.integration.ts index edaa8ff5cb4..d49700d1068 100644 --- a/apps/sim/lib/desktop/application/executor.integration.ts +++ b/apps/sim/lib/desktop/application/executor.integration.ts @@ -37,6 +37,7 @@ import { createDeferred } from '@sim/testing/helpers/deferred' import { generateId } from '@sim/utils/id' import { compareStrings } from '@sim/utils/string' import { eq, inArray, sql } from 'drizzle-orm' +import type { PgUpdateSetSource } from 'drizzle-orm/pg-core' import { closeRedisConnection, getRedisClient } from '@/lib/core/config/redis' import type { DbTransaction } from '@/lib/db/types' import { @@ -54,8 +55,10 @@ import { } from '@/lib/desktop/executor/errors' import { isDesktopPresent } from '@/lib/desktop/executor/presence' import { claimDesktopToolCall, requestRunStop } from '@/lib/mothership/async-runs/repository' +import { TOOL_WATCHDOG_RESUME_GRACE_MS } from '@/lib/mothership/constants' import { waitForToolConfirmation } from '@/lib/mothership/persistence/tool-confirm' import { unsealClientToolCompletion } from '@/lib/mothership/request/tools/client-completion-seal.server' +import { settleAbandonedDesktopToolCalls } from '@/lib/mothership/request/tools/desktop-wait' const CAPABILITIES = { executor: 1, browser: true, terminal: true, localFiles: true } @@ -556,6 +559,7 @@ describe.runIf(Boolean(redisUrl))('desktop background executor protocol', () => const toolCallId = await pendingCall(run.runId) const { executionToken } = await claim(desktop.principal, desktop.deviceId, toolCallId) const input = { deviceId: desktop.deviceId, toolCallId, executionToken } + expect((await row(toolCallId)).executionDeadlineAt).toBeNull() await db .update(copilotAsyncToolCalls) @@ -583,6 +587,41 @@ describe.runIf(Boolean(redisUrl))('desktop background executor protocol', () => ).rejects.toBeInstanceOf(DesktopCallRevokedError) }) + it('stops renewing a call once the resume gate gave up on its deadline, then settles it as outcome unknown', async () => { + const desktop = await signedInDesktop() + const run = await boundRun(desktop) + const toolCallId = await pendingCall(run.runId) + const setRow = (fields: PgUpdateSetSource) => + db + .update(copilotAsyncToolCalls) + .set(fields) + .where(eq(copilotAsyncToolCalls.toolCallId, toolCallId)) + const deadlinePassedBy = (seconds: number) => + setRow({ executionDeadlineAt: sql`clock_timestamp() - ${seconds} * interval '1 second'` }) + await setRow({ executionDeadlineAt: sql`clock_timestamp() + interval '10 minutes'` }) + const { executionToken } = await claim(desktop.principal, desktop.deviceId, toolCallId) + const input = { deviceId: desktop.deviceId, toolCallId, executionToken } + + // Inside the gate's grace past the deadline, the device keeps the call. + await deadlinePassedBy(TOOL_WATCHDOG_RESUME_GRACE_MS / 1000 - 10) + await expect( + renewDesktopToolLease.execute({ principal: desktop.principal, input }) + ).resolves.toEqual({ renewed: true }) + + await deadlinePassedBy(TOOL_WATCHDOG_RESUME_GRACE_MS / 1000 + 1) + await expect( + renewDesktopToolLease.execute({ principal: desktop.principal, input }) + ).rejects.toBeInstanceOf(DesktopCallRevokedError) + + // Unrenewed, the lease lapses, and the cleanup settles the call as it would any lapse. + await setRow({ executionLeaseExpiresAt: sql`clock_timestamp() - interval '1 second'` }) + await settleAbandonedDesktopToolCalls(0) + expect(await row(toolCallId)).toMatchObject({ + status: 'failed', + error: expect.stringContaining('It may already have taken effect'), + }) + }) + it('after Stop, refuses the stopped calls, revokes the running one and lists it to cancel', async () => { const desktop = await signedInDesktop() const run = await boundRun(desktop) diff --git a/apps/sim/lib/execution/durable-secret-provenance-telemetry.test.ts b/apps/sim/lib/execution/durable-secret-provenance-telemetry.test.ts index 6d1d2c22870..549e63f6702 100644 --- a/apps/sim/lib/execution/durable-secret-provenance-telemetry.test.ts +++ b/apps/sim/lib/execution/durable-secret-provenance-telemetry.test.ts @@ -2,6 +2,7 @@ import { getMockLogger } from '@sim/testing/mocks/logger.mock' import { describe, expect, it } from 'vitest' import { reportDurableSecretProvenanceRefusal, + reportDurableSecretProvenanceRefusals, reportDurableSecretProvenanceWrite, } from '@/lib/execution/durable-secret-provenance-telemetry' @@ -42,14 +43,66 @@ describe('durable secret provenance telemetry', () => { entries: [{ encryptedValue: 'private ciphertext' }], } reportDurableSecretProvenanceRefusal(report) - expect(logger.error).toHaveBeenCalledExactlyOnceWith( + expect(logger.warn).toHaveBeenCalledExactlyOnceWith( 'Refusing unavailable durable secret provenance', { surface: 'workspace-file', cause: 'workspace-file-opaque-secret-content', + refusedCount: 1, workspaceId: 'workspace-1', resourceId: 'file-1', } ) }) + it('reports a batch once per surface and cause, keeping every id paired with its cause', () => { + logger.warn.mockClear() + const reports = Array.from({ length: 163 }, (_, index) => ({ + surface: 'knowledge' as const, + cause: + index < 160 + ? ('knowledge-chunk-source-unavailable' as const) + : ('knowledge-opaque-secret-content' as const), + workspaceId: 'workspace-1', + resourceId: `chunk-${index}`, + })) + reportDurableSecretProvenanceRefusals(reports) + expect(logger.warn).toHaveBeenCalledTimes(2) + expect(logger.warn).toHaveBeenNthCalledWith( + 1, + 'Refusing unavailable durable secret provenance', + { + surface: 'knowledge', + cause: 'knowledge-chunk-source-unavailable', + refusedCount: 160, + workspaceId: 'workspace-1', + resourceIds: reports.slice(0, 160).map((report) => report.resourceId), + } + ) + expect(logger.warn).toHaveBeenNthCalledWith( + 2, + 'Refusing unavailable durable secret provenance', + { + surface: 'knowledge', + cause: 'knowledge-opaque-secret-content', + refusedCount: 3, + workspaceId: 'workspace-1', + resourceIds: ['chunk-160', 'chunk-161', 'chunk-162'], + } + ) + expect(logger.error).not.toHaveBeenCalled() + }) + + it('caps the ids of one very large group and says so', () => { + logger.warn.mockClear() + reportDurableSecretProvenanceRefusals( + Array.from({ length: 250 }, (_, index) => ({ + surface: 'table-row' as const, + cause: 'table-row-provenance-unavailable' as const, + resourceId: `row-${index}`, + })) + ) + const [[, fields]] = logger.warn.mock.calls + expect(fields).toMatchObject({ refusedCount: 250, resourceIdsTruncated: true }) + expect(fields.resourceIds).toHaveLength(200) + }) }) diff --git a/apps/sim/lib/execution/durable-secret-provenance-telemetry.ts b/apps/sim/lib/execution/durable-secret-provenance-telemetry.ts index 49c4c19d9f3..415665771bd 100644 --- a/apps/sim/lib/execution/durable-secret-provenance-telemetry.ts +++ b/apps/sim/lib/execution/durable-secret-provenance-telemetry.ts @@ -70,18 +70,58 @@ export function reportDurableSecretProvenanceWrite( }) } -/** Reports a durable read refusal without exposing content or secret values. */ +/** + * Reports a durable read refusal without exposing content or secret values. A refusal is policy + * applied to content that lacks provenance, not a fault, so it is a warning. + */ export function reportDurableSecretProvenanceRefusal( report: DurableSecretProvenanceRefusalReport ): void { - persistenceLogger.error('Refusing unavailable durable secret provenance', { + persistenceLogger.warn('Refusing unavailable durable secret provenance', { surface: report.surface, cause: report.cause, + refusedCount: 1, ...(report.workspaceId ? { workspaceId: report.workspaceId } : {}), ...(report.resourceId ? { resourceId: report.resourceId } : {}), }) } +/** Ids one grouped refusal lists, so a page of them stays one line. */ +const REFUSAL_RESOURCE_ID_LIMIT = 200 + +/** + * Reports the refusals of one read with one line per surface and cause, each + * carrying the same top-level `surface` and `cause` as a single refusal, its `refusedCount`, and + * the refused ids, so each id stays paired with its cause. + */ +export function reportDurableSecretProvenanceRefusals( + reports: readonly DurableSecretProvenanceRefusalReport[] +): void { + const groups = new Map() + for (const report of reports) { + const key = JSON.stringify([report.surface, report.cause, report.workspaceId]) + const group = groups.get(key) + if (group) group.push(report) + else groups.set(key, [report]) + } + for (const group of groups.values()) { + const [first] = group + if (group.length === 1) { + reportDurableSecretProvenanceRefusal(first) + continue + } + const resourceIds = group.flatMap((report) => (report.resourceId ? [report.resourceId] : [])) + persistenceLogger.warn('Refusing unavailable durable secret provenance', { + surface: first.surface, + cause: first.cause, + refusedCount: group.length, + ...(first.workspaceId ? { workspaceId: first.workspaceId } : {}), + resourceIds: resourceIds.slice(0, REFUSAL_RESOURCE_ID_LIMIT), + ...(resourceIds.length > REFUSAL_RESOURCE_ID_LIMIT ? { resourceIdsTruncated: true } : {}), + }) + } +} + export interface DurableSecretProvenanceUnrecordedReport { surface: DurableSecretProvenanceSurface workspaceId?: string diff --git a/apps/sim/lib/internal/file/operations.test.ts b/apps/sim/lib/internal/file/operations.test.ts index aa228f2efb1..1c476fc66c5 100644 --- a/apps/sim/lib/internal/file/operations.test.ts +++ b/apps/sim/lib/internal/file/operations.test.ts @@ -1278,55 +1278,6 @@ describe('file manage operations', () => { expect(mockUploadWorkspaceFile).toHaveBeenCalled() }) - it('downgrades provenance when overwriting a file owned by another user', async () => { - const existing = workspaceFile('report', 'other-user') - mockResolveWorkspaceFileReference.mockResolvedValue(existing) - mockUpdateWorkspaceFileContent.mockResolvedValue(existing) - - const response = await POST( - createMockRequest( - 'POST', - { - operation: 'write', - workspaceId: 'workspace-1', - fileName: 'report.txt', - content: 'secret-value', - overwrite: true, - __privateSecretProvenance: { - version: 1, - complete: true, - selections: [ - { - key: 'content', - provenance: { - version: 1, - complete: true, - entries: [{ name: 'TOKEN', encryptedValue: 'encrypted-token' }], - scope: { userId: 'user-1', workspaceId: 'workspace-1' }, - }, - }, - ], - }, - }, - PRIVATE_SECRET_PROVENANCE_HEADER - ) - ) - - expect(response.status).toBe(200) - expect(mockUpdateWorkspaceFileContent).toHaveBeenCalledWith( - 'workspace-1', - 'report', - 'user-1', - Buffer.from('secret-value'), - 'text/plain', - { - version: { source: 'workflow', authorUserId: 'user-1' }, - expectedUpdatedAt: CONTENT_UPDATED_AT, - secretProvenancePolicy: { mode: 'replace', provenance: { status: 'unknown' } }, - } - ) - }) - it('atomically binds append provenance to the exact predecessor version', async () => { const existing = workspaceFile('file-1') mockResolveWorkspaceFileReference.mockResolvedValue(existing) diff --git a/apps/sim/lib/internal/file/operations.ts b/apps/sim/lib/internal/file/operations.ts index aacdd31bc1f..2fc26305e70 100644 --- a/apps/sim/lib/internal/file/operations.ts +++ b/apps/sim/lib/internal/file/operations.ts @@ -1412,14 +1412,6 @@ export async function executeFileManageOperation( leafName, }) if (existing) { - // Writing into a file someone else owns must not hand its owner an - // exact, re-resolvable secret lineage, exactly as appending does. - const overwriteProvenance = - writeProvenance?.status === 'exact' && - writeProvenance.entries.length > 0 && - existing.uploadedBy !== userId - ? { status: 'unknown' as const } - : writeProvenance const { file: overwritten } = await updateWorkspaceFileContent.execute({ principal, input: { @@ -1431,7 +1423,7 @@ export async function executeFileManageOperation( provenanceMode: 'replace_empty', expectedUpdatedAt: existing.contentUpdatedAt ?? undefined, expectedRevision, - ...(overwriteProvenance ? { secretProvenance: overwriteProvenance } : {}), + ...(writeProvenance ? { secretProvenance: writeProvenance } : {}), }, }) @@ -1657,14 +1649,9 @@ export async function executeFileManageOperation( ) } const appendedProvenance = appendedResolution.provenanceBySelection?.get('content') - const secretProvenance = - appendedProvenance?.status === 'exact' && - appendedProvenance.entries.length > 0 && - existing.uploadedBy !== userId - ? { status: 'unknown' as const } - : appendedProvenance - ? mergeWorkspaceFileSecretProvenance(existingProvenance, appendedProvenance) - : undefined + const secretProvenance = appendedProvenance + ? mergeWorkspaceFileSecretProvenance(existingProvenance, appendedProvenance) + : undefined const { content: existingBuffer } = await readWorkspaceFileContent.execute({ principal, input: { diff --git a/apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts b/apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts index 300a146f850..77eac5d685a 100644 --- a/apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts @@ -15,6 +15,7 @@ import { workspace, } from '@sim/db/schema' import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' +import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNotNull, isNull, sql } from 'drizzle-orm' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -113,6 +114,78 @@ describe('independent connector saves in one knowledge base', () => { await db.$client.end() }) + it('keeps retained chunk tags consistent during metadata-only connector refresh', async () => { + const item = await source('retry hash') + const chunkId = generateId() + const refreshedTags = { + tag1: 'current', + tag2: null, + number1: 3.5, + boolean1: true, + date1: new Date('2026-10-01T00:00:00.000Z'), + } + await db + .update(document) + .set({ tag1: 'old', tag2: 'cleared', chunkCount: 1 }) + .where(eq(document.id, item.documentId)) + await db.insert(embedding).values({ + id: chunkId, + knowledgeBaseId: ids.knowledgeBaseId, + documentId: item.documentId, + chunkIndex: 0, + chunkHash: sha256Hex('retained content'), + content: 'retained content', + contentLength: 16, + tokenCount: 4, + startOffset: 0, + endOffset: 16, + embedding: Array(1536).fill(0.2), + tag1: 'old', + tag2: 'cleared', + }) + await persistHashOnlyUpdates( + ids.knowledgeBaseId, + ids.connectorId, + [ + { + existingId: item.documentId, + externalId: item.extDoc.externalId, + contentHash: 'refreshed-hash', + sourceMetadata: { sourceUrl: null, sourceModifiedAt: null, ...refreshedTags }, + }, + ], + { stillHeld: () => stillHoldsSyncLock(ids.connectorId, ids.lockId) } + ) + expect( + await db + .select({ + tag1: document.tag1, + tag2: document.tag2, + number1: document.number1, + boolean1: document.boolean1, + date1: document.date1, + count: document.chunkCount, + status: document.processingStatus, + }) + .from(document) + .where(eq(document.id, item.documentId)) + ).toEqual([{ ...refreshedTags, count: 1, status: 'completed' }]) + expect( + await db + .select({ + tag1: embedding.tag1, + tag2: embedding.tag2, + number1: embedding.number1, + boolean1: embedding.boolean1, + date1: embedding.date1, + content: embedding.content, + id: embedding.id, + }) + .from(embedding) + .where(eq(embedding.documentId, item.documentId)) + ).toEqual([{ ...refreshedTags, content: 'retained content', id: chunkId }]) + }) + async function source(kind: SaveKind) { const item = { documentId: generateId(), diff --git a/apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts b/apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts index 7a411efc12a..833fdf234ee 100644 --- a/apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts @@ -12,6 +12,7 @@ import { knowledgeBase, organization, outboxEvent, + permissions, user, workspace, workspaceFiles, @@ -19,7 +20,7 @@ import { import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { remoteSandboxMock, remoteSandboxMockFns } from '@sim/testing/mocks/remote-sandbox.mock' import { generateId } from '@sim/utils/id' -import { eq, inArray, sql } from 'drizzle-orm' +import { and, eq, inArray, sql } from 'drizzle-orm' import JSZip from 'jszip' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' @@ -64,7 +65,10 @@ vi.mock('@/lib/embeddings', async () => ({ })) import { functionExecuteBodySchema } from '@/lib/api/contracts' -import { fileManageDecompressBodySchema } from '@/lib/api/contracts/tools/file' +import { + fileManageBodySchema, + fileManageDecompressBodySchema, +} from '@/lib/api/contracts/tools/file' import { observeContentDelivery } from '@/lib/core/application' import { processOutboxEventById } from '@/lib/core/outbox/service' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' @@ -990,9 +994,121 @@ describe('execution archive durable provenance', () => { expect(await isOpaqueWorkspaceFileEgressSafe(ids.workspaceId, source.identity)).toBe(true) }) - it.each([false, true])( - 'refuses tracked unknown execution attachments with historical metadata (archivedOnly=%s)', - async (archivedOnly) => { + it.each(['append', 'write'] as const)( + 'preserves encrypted origins when another member performs %s', + async (operation) => { + const ids = await seed() + await db + .update(permissions) + .set({ permissionType: 'write' }) + .where(and(eq(permissions.userId, ids.bobId), eq(permissions.entityId, ids.workspaceId))) + const priorSecret = 'fixture-prior-origin-secret' + const previous = { + status: 'exact' as const, + entries: [ + { + encryptedValue: (await encryptSecret(priorSecret)).encrypted, + sourceUserId: ids.aliceId, + sourceWorkspaceId: ids.workspaceId, + name: 'PRIOR_TOKEN', + }, + ], + } + const source = await uploadWorkspaceFile( + ids.workspaceId, + ids.aliceId, + Buffer.from(priorSecret), + `${generateId()}.txt`, + 'text/plain', + { secretProvenance: previous, notifyWorkspaceChange: false } + ) + const response = await executeFileManageOperation( + fileManageBodySchema.parse({ + operation, + workspaceId: ids.workspaceId, + fileName: source.name, + content: FIXTURE_SECRET, + ...(operation === 'write' ? { overwrite: true } : {}), + [PRIVATE_SECRET_PROVENANCE_FIELD]: { + version: 1, + complete: true, + selections: [ + { + key: 'content', + provenance: { + version: 1, + complete: true, + scope: { userId: ids.bobId, workspaceId: ids.workspaceId }, + entries: [ + { + name: 'NEW_TOKEN', + encryptedValue: (await encryptSecret(FIXTURE_SECRET)).encrypted, + }, + ], + }, + }, + ], + }, + }), + { + principal: createWorkspaceFileDelegatedPrincipal({ + serviceId: 'executor', + subjectUserId: ids.bobId, + workspaceId: ids.workspaceId, + delegationId: generateId(), + executionId: ids.executionId, + }), + workspaceId: ids.workspaceId, + attributedUserId: ids.bobId, + fileAccessUserId: ids.bobId, + workflowId: ids.workflowId, + executionId: ids.executionId, + headers: new Headers({ + [PRIVATE_SECRET_PROVENANCE_HEADER]: PRIVATE_SECRET_PROVENANCE_BUNDLE_V1, + }), + requestId: generateId(), + } + ) + expect(response.status, await response.text()).toBe(200) + const record = await getWorkspaceFile(ids.workspaceId, source.id) + if (!record) throw new Error('Edited file disappeared') + const bytes = await downloadFile({ key: record.key, context: 'workspace' }) + expect(bytes.toString()).toBe( + operation === 'append' ? priorSecret + FIXTURE_SECRET : FIXTURE_SECRET + ) + const registry = new ResolvedSecretTraceRegistry([], { + userId: ids.bobId, + workspaceId: ids.workspaceId, + }) + expect( + await importWorkspaceFileSecretProvenanceForRuntime({ + workspaceId: ids.workspaceId, + identity: { + fileId: record.id, + key: record.key, + context: 'workspace', + contentUpdatedAt: record.contentUpdatedAt ?? undefined, + }, + registry, + }) + ).toBe(true) + const shown = projectResolvedSecretModelContent(bytes.toString(), registry) + expect(shown.safe).toBe(true) + expect(JSON.stringify(shown)).not.toContain(FIXTURE_SECRET) + expect(JSON.stringify(shown)).not.toContain(priorSecret) + expect(JSON.stringify(shown)).toContain('{{NEW_TOKEN}}') + if (operation === 'append') expect(JSON.stringify(shown)).toContain('[REDACTED_SECRET]') + } + ) + + it.each([ + ['execution', false], + ['execution', true], + ['workspace', true], + ['mothership', true], + ] as const)( + 'retains refusal evidence for %s attachments (archivedOnly=%s)', + async (context, archivedOnly) => { const ids = await seed() const file = await uploadExecutionFile( ids, @@ -1005,7 +1121,7 @@ describe('execution archive durable provenance', () => { if (archivedOnly) { await db .update(workspaceFiles) - .set({ deletedAt: new Date() }) + .set({ deletedAt: new Date(), context }) .where(eq(workspaceFiles.key, file.key)) } else { await db.insert(workspaceFiles).values({ diff --git a/apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts b/apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts index c2586ebf5ce..4d82f09d945 100644 --- a/apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts @@ -7,7 +7,16 @@ import { rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' import { db } from '@sim/db' -import { document, embedding, knowledgeBase, organization, user, workspace } from '@sim/db/schema' +import { + document, + embedding, + knowledgeBase, + knowledgeBaseTagDefinitions, + organization, + user, + workspace, +} from '@sim/db/schema' +import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' import { deleteWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { generateId } from '@sim/utils/id' import { eq, inArray } from 'drizzle-orm' @@ -26,14 +35,21 @@ vi.mock('@/lib/knowledge/documents/processor', () => ({ vi.mock('@/lib/knowledge/embeddings', () => ({ generateEmbeddings: fixtures.embeddings })) import { resolveBillingAttribution } from '@/lib/billing/core/attribution' +import { observeContentDelivery } from '@/lib/core/application' import * as embeddingClient from '@/lib/embeddings/client' import { createKnowledgeAclFixtureIds, seedKnowledgeAclFixture, } from '@/lib/knowledge/__integration__/seed-source-access-fixture' +import { KNOWLEDGE_DELEGATION_AUDIENCE } from '@/lib/knowledge/application/authorization' +import { createKnowledgeChunk, listKnowledgeChunks } from '@/lib/knowledge/application/chunks' import { createContentSyncLease } from '@/lib/knowledge/connectors/sync-lock' import { addDocument } from '@/lib/knowledge/connectors/sync-persistence' -import { processDocumentAsync } from '@/lib/knowledge/documents/service' +import { processDocumentAsync, updateDocument } from '@/lib/knowledge/documents/service' +import { deleteAllTagDefinitions } from '@/lib/knowledge/tags/service' +import { createCopilotChatPrincipal } from '@/lib/mothership/auth/application-delegation' +import { createRegistryContentGate } from '@/lib/mothership/content-delivery/registry-gate' +import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' /** Advisory lock key that parks a processing transaction inside its embedding insert. */ const EMBEDDING_GATE = 7_310_191 @@ -137,6 +153,170 @@ describe('document processing commit lock scope', () => { return resolveBillingAttribution({ actorUserId: ids.aliceId, workspaceId: ids.workspaceId }) } + async function expectCurrentChunks( + documentId: string, + contents: string[], + expectedTag: string | null = 'current' + ) { + const principal = createCopilotChatPrincipal( + { userId: ids.aliceId, workspaceId: ids.workspaceId }, + KNOWLEDGE_DELEGATION_AUDIENCE + ) + const result = await observeContentDelivery( + createRegistryContentGate({ + registry: new ResolvedSecretTraceRegistry([], { + userId: ids.aliceId, + workspaceId: ids.workspaceId, + }), + actorUserId: ids.aliceId, + workspaceId: ids.workspaceId, + }), + () => + listKnowledgeChunks.execute({ + principal, + input: { knowledgeBaseId: ids.knowledgeBaseId, documentId }, + }) + ) + expect(result.chunks.map((row) => row.content).sort()).toEqual([...contents].sort()) + expect(result.chunks.every((row) => row.tag1 === expectedTag)).toBe(true) + } + + it('uses metadata committed while processing waits for embeddings', async () => { + const file = await addConnectorDocument('metadata-during-provider') + await db.update(document).set({ connectorId: null }).where(eq(document.id, file.documentId)) + fixtures.embeddings.mockImplementationOnce(async () => { + await updateDocument(file.documentId, { tag1: 'current' }, 'metadata-during-provider') + return embeddingResult + }) + expect( + await processDocumentAsync(ids.knowledgeBaseId, file.documentId, file, {}, await billing()) + ).toEqual({ outcome: 'indexed' }) + await expectCurrentChunks( + file.documentId, + chunks.map((chunk) => chunk.text) + ) + }) + + it.each(['metadata edit', 'tag deletion'] as const)( + 'applies a waiting %s to the chunks committed by processing', + async (kind) => { + const file = await addConnectorDocument('metadata-during-commit') + await db.update(document).set({ connectorId: null }).where(eq(document.id, file.documentId)) + if (kind === 'tag deletion') { + await db + .insert(knowledgeBaseTagDefinitions) + .values([ + { + id: generateId(), + knowledgeBaseId: ids.knowledgeBaseId, + tagSlot: 'tag1', + displayName: 'Fixture text', + fieldType: 'text', + }, + { + id: generateId(), + knowledgeBaseId: ids.knowledgeBaseId, + tagSlot: 'number1', + displayName: 'Fixture number', + fieldType: 'number', + }, + ]) + .onConflictDoNothing() + await db + .update(document) + .set({ tag1: 'old', number1: 3 }) + .where(eq(document.id, file.documentId)) + } + const gate = postgres(readTestDatabaseUrl(), { max: 1, onnotice: () => undefined }) + let processing: ReturnType | undefined + let editing: Promise | undefined + try { + await gate`SELECT pg_advisory_lock(${EMBEDDING_GATE})` + await installEmbeddingProbe( + file.documentId, + `PERFORM pg_advisory_xact_lock_shared(${EMBEDDING_GATE});` + ) + processing = processDocumentAsync( + ids.knowledgeBaseId, + file.documentId, + file, + {}, + await billing() + ) + let processingPid = 0 + await vi.waitFor( + async () => { + const [row] = await gate>` + SELECT pid FROM pg_locks WHERE locktype = 'advisory' AND objid = ${EMBEDDING_GATE} AND NOT granted` + expect(row?.pid).toBeGreaterThan(0) + processingPid = row?.pid ?? 0 + }, + { timeout: 10_000, interval: 20 } + ) + editing = + kind === 'metadata edit' + ? updateDocument(file.documentId, { tag1: 'current' }, 'metadata-during-commit') + : deleteAllTagDefinitions(ids.knowledgeBaseId, 'tag-deletion-during-commit') + await vi.waitFor( + async () => { + const [row] = await gate>` + SELECT EXISTS (SELECT 1 FROM pg_stat_activity WHERE ${processingPid} = ANY(pg_blocking_pids(pid))) AS waiting` + expect(row?.waiting).toBe(true) + }, + { timeout: 10_000, interval: 20 } + ) + } finally { + await gate`SELECT pg_advisory_unlock(${EMBEDDING_GATE})` + try { + await Promise.all([processing, editing]) + } finally { + await gate.end() + } + } + await expectCurrentChunks( + file.documentId, + chunks.map((chunk) => chunk.text), + kind === 'metadata edit' ? 'current' : null + ) + if (kind === 'tag deletion') { + const retained = await db + .select({ tag1: embedding.tag1, number1: embedding.number1 }) + .from(embedding) + .where(eq(embedding.documentId, file.documentId)) + expect(retained).toHaveLength(chunks.length) + expect(retained.every((row) => row.tag1 === null && row.number1 === null)).toBe(true) + expect( + await db + .select() + .from(knowledgeBaseTagDefinitions) + .where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, ids.knowledgeBaseId)) + ).toEqual([]) + } + } + ) + + it('uses current document tags when a manual chunk finishes its provider request', async () => { + const file = await addConnectorDocument('manual-metadata-during-provider') + await db + .update(document) + .set({ connectorId: null, processingStatus: 'completed' }) + .where(eq(document.id, file.documentId)) + fixtures.embeddings.mockImplementationOnce(async () => { + await updateDocument(file.documentId, { tag1: 'current' }, 'manual-metadata-during-provider') + return { ...embeddingResult, embeddings: [embeddingResult.embeddings[0]] } + }) + await createKnowledgeChunk.execute({ + principal: { kind: 'session', userId: ids.aliceId, sessionId: generateId() }, + input: { + knowledgeBaseId: ids.knowledgeBaseId, + documentId: file.documentId, + content: 'Manual chunk content', + resolveContentProvenance: () => ({ status: 'exact', entries: [] }), + }, + }) + await expectCurrentChunks(file.documentId, ['Manual chunk content']) + }) + it('holds no knowledge_connector lock while writing embeddings', async () => { const file = await addConnectorDocument('lock-scope-fixture') await installConnectorLockProbe() @@ -174,8 +354,8 @@ describe('document processing commit lock scope', () => { * The embedding insert waits on an advisory lock the test holds, so the processing * transaction is parked mid-write while a second connection commits the deletion. */ - const gate = postgres(process.env.DATABASE_URL!, { max: 1, onnotice: () => undefined }) - const deleter = postgres(process.env.DATABASE_URL!, { max: 1, onnotice: () => undefined }) + const gate = postgres(readTestDatabaseUrl(), { max: 1, onnotice: () => undefined }) + const deleter = postgres(readTestDatabaseUrl(), { max: 1, onnotice: () => undefined }) try { await gate`SELECT pg_advisory_lock(${EMBEDDING_GATE})` await installEmbeddingProbe( diff --git a/apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts b/apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts index b5f53a8a433..5f386ac758e 100644 --- a/apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts @@ -27,10 +27,16 @@ vi.mock('@/lib/uploads/core/setup.server', () => ({ }, })) +import { encryptSecret } from '@/lib/core/security/encryption' import { createKnowledgeAclFixtureIds, seedKnowledgeAclFixture, } from '@/lib/knowledge/__integration__/seed-source-access-fixture' +import { + executeChatFileBlobCopies, + persistChatFileCopies, + planChatFileCopies, +} from '@/lib/mothership/chat/fork-chat-files' import { generateWorkspaceFileKey, trackChatUpload, @@ -48,8 +54,11 @@ import { snapshotWorkspaceFileSecretProvenanceInTx, type WorkspaceFileSecretProvenance, } from '@/lib/uploads/contexts/workspace/file-secret-provenance' -import { uploadFile } from '@/lib/uploads/core/storage-service' +import { downloadFile, uploadFile } from '@/lib/uploads/core/storage-service' import { readWorkspaceFileText } from '@/lib/workspace-files/application/read-workspace-file-text' +import { executeForkFileBlobCopies } from '@/ee/workspace-forking/lib/copy/copy-files' +import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' +import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' const fixtures: ReturnType[] = [] const CONTENT = 'The same uploaded bytes remain readable after saving.\n' @@ -139,6 +148,104 @@ afterAll(async () => { }) describe('chat upload reads racing with save_upload', () => { + it.each(['creator', 'workspace'] as const)( + 'keeps byte-identical forked files usable with anonymized encrypted origins across a different %s', + async (kind) => { + const ids = await seedUpload() + const encryptedValue = (await encryptSecret(CONTENT)).encrypted + await db.transaction((tx) => + replaceWorkspaceFileSecretProvenanceInTx(tx, ids.file.id, ids.file.contentUpdatedAt, { + status: 'exact', + entries: [ + { + encryptedValue, + sourceUserId: ids.aliceId, + sourceWorkspaceId: ids.workspaceId, + name: 'ORIGINAL_TOKEN', + }, + ], + }) + ) + const [source] = await db + .select() + .from(workspaceFiles) + .where(eq(workspaceFiles.id, ids.file.id)) + let copyId: string | undefined + let targetWorkspaceId = ids.workspaceId + let recipientId = ids.bobId + if (kind === 'workspace') { + const target = await seedUpload() + targetWorkspaceId = target.workspaceId + recipientId = target.aliceId + copyId = `wf_${generateId()}` + const copied = await executeForkFileBlobCopies( + [ + { + sourceFileId: source.id, + sourceContentUpdatedAtMs: source.contentUpdatedAt.getTime(), + sourceKey: source.key, + targetKey: generateWorkspaceFileKey(targetWorkspaceId, 'copied.txt'), + targetFileId: copyId, + workspaceId: targetWorkspaceId, + userId: recipientId, + context: 'mothership', + fileName: 'copied.txt', + displayName: null, + contentType: 'text/plain', + size: Buffer.byteLength(CONTENT), + }, + ], + 'copy-provenance-regression' + ) + expect(copied).toMatchObject({ copied: 1, failed: 0 }) + } else { + const newChatId = generateId() + await db.insert(copilotChats).values({ + id: newChatId, + userId: recipientId, + workspaceId: targetWorkspaceId, + type: 'mothership', + }) + const plan = planChatFileCopies({ + rows: [source], + newChatId, + userId: recipientId, + now: new Date(), + }) + const copied = await executeChatFileBlobCopies(plan.blobTasks) + expect(copied.failed).toBe(0) + await db.transaction((tx) => persistChatFileCopies(tx, plan, new Set(copied.failedCopyIds))) + copyId = plan.idMap.get(source.id) + } + if (!copyId) throw new Error('Fork did not map its file') + const [copy] = await db.select().from(workspaceFiles).where(eq(workspaceFiles.id, copyId)) + const bytes = await downloadFile({ key: copy.key, context: 'mothership' }) + expect(bytes.toString()).toBe(CONTENT) + const registry = new ResolvedSecretTraceRegistry([], { + userId: recipientId, + workspaceId: targetWorkspaceId, + }) + expect( + await importWorkspaceFileSecretProvenanceForModelView({ + workspaceId: targetWorkspaceId, + identity: { + fileId: copy.id, + key: copy.key, + context: 'mothership', + contentUpdatedAt: copy.contentUpdatedAt, + }, + registry, + view: 'complete', + value: bytes.toString(), + }) + ).toBe(true) + expect(projectResolvedSecretModelContent(bytes.toString(), registry)).toMatchObject({ + safe: true, + value: '[REDACTED_SECRET]', + }) + } + ) + it.each(['legacy', 'exact', 'unrecorded'] as const)( 'keeps a %s authorized upload read valid across promotion', async (kind) => { diff --git a/apps/sim/lib/knowledge/application/chunks.ts b/apps/sim/lib/knowledge/application/chunks.ts index 719f87d56ed..47a680e495a 100644 --- a/apps/sim/lib/knowledge/application/chunks.ts +++ b/apps/sim/lib/knowledge/application/chunks.ts @@ -107,29 +107,6 @@ function requireChunkWritable(context: ActiveKnowledgeDocumentContext): void { } } -function documentTags(context: ActiveKnowledgeDocumentContext) { - const document = context.document - return { - tag1: document.tag1 ?? null, - tag2: document.tag2 ?? null, - tag3: document.tag3 ?? null, - tag4: document.tag4 ?? null, - tag5: document.tag5 ?? null, - tag6: document.tag6 ?? null, - tag7: document.tag7 ?? null, - number1: document.number1 ?? null, - number2: document.number2 ?? null, - number3: document.number3 ?? null, - number4: document.number4 ?? null, - number5: document.number5 ?? null, - date1: document.date1 ?? null, - date2: document.date2 ?? null, - boolean1: document.boolean1 ?? null, - boolean2: document.boolean2 ?? null, - boolean3: document.boolean3 ?? null, - } -} - export const listKnowledgeChunks = defineAuthorizedKnowledgeUseCase({ operation: knowledgeOperations.listChunks, resolveContext: ({ @@ -226,7 +203,6 @@ export const createKnowledgeChunk = defineAuthorizedKnowledgeUseCase({ createChunk( context.knowledgeBaseId, context.documentId, - documentTags(context), { content: input.content, enabled: input.enabled }, generateRequestId(), context.workspaceId, diff --git a/apps/sim/lib/knowledge/application/slack-search/assistant.integration.ts b/apps/sim/lib/knowledge/application/slack-search/assistant.integration.ts index 8b0374cddcd..1a52458d9de 100644 --- a/apps/sim/lib/knowledge/application/slack-search/assistant.integration.ts +++ b/apps/sim/lib/knowledge/application/slack-search/assistant.integration.ts @@ -139,8 +139,11 @@ function job() { } } -/** Runs one Slack turn in a fresh private chat and returns the run it recorded. */ -async function slackTurn() { +/** + * Runs one Slack turn in a fresh private chat and returns the run it recorded. `deletedAt` + * stands for a delete committed after the turn resolved its chat. + */ +async function slackTurn(deletedAt?: Date) { const chatId = generateId() const turnId = generateId() hoisted.chat = { id: chatId, model: 'default' } @@ -150,6 +153,7 @@ async function slackTurn() { userId: hoisted.userId, organizationId: hoisted.organizationId, type: 'mothership', + deletedAt, }) const controller = new AbortController() hoisted.controller = controller @@ -237,6 +241,17 @@ describe('Slack Assistant run record', () => { expect(run.completedAt).not.toBeNull() }) + it('never starts the worker for a turn whose chat was deleted before its run', async () => { + mockRunHeadlessCopilotLifecycle.mockClear() + + const { run, outcome } = await slackTurn(new Date()) + + expect(mockRunHeadlessCopilotLifecycle).not.toHaveBeenCalled() + expect(outcome).toBeInstanceOf(Error) + expect(run.status).toBe('cancelled') + expect(run.toolAdmissionClosedAt).not.toBeNull() + }) + it('records a failed turn as an error', async () => { mockRunHeadlessCopilotLifecycle.mockResolvedValueOnce({ success: false, diff --git a/apps/sim/lib/knowledge/application/slack-search/assistant.ts b/apps/sim/lib/knowledge/application/slack-search/assistant.ts index b211965fbdd..dad4f2e0c7f 100644 --- a/apps/sim/lib/knowledge/application/slack-search/assistant.ts +++ b/apps/sim/lib/knowledge/application/slack-search/assistant.ts @@ -49,7 +49,7 @@ import { startAbortPoller, unregisterActiveStream, } from '@/lib/mothership/request/session/abort' -import { isExplicitStopReason } from '@/lib/mothership/request/session/abort-reason' +import { AbortReason, isExplicitStopReason } from '@/lib/mothership/request/session/abort-reason' import type { OrchestratorResult } from '@/lib/mothership/request/types' import { organizationRoutes } from '@/lib/navigation/paths' import { SlackSearchAssistantStream } from '@/lib/slack-search/assistant-stream' @@ -206,6 +206,11 @@ export async function runSlackSearchAssistant( }) if (!run) throw new Error('Could not persist Assistant execution') runId = run.id + /** Admission found the turn stopped or its chat deleted: it ends as that Stop would. */ + if (run.status === 'cancelled') { + controller.abort(AbortReason.UserStop) + controller.signal.throwIfAborted() + } const responseStream = new SlackSearchAssistantStream({ token: secret.botToken, channel: job.message.channelId, diff --git a/apps/sim/lib/knowledge/application/slack-search/installations.ts b/apps/sim/lib/knowledge/application/slack-search/installations.ts index 6c1c64cbe13..5348d3c545e 100644 --- a/apps/sim/lib/knowledge/application/slack-search/installations.ts +++ b/apps/sim/lib/knowledge/application/slack-search/installations.ts @@ -7,6 +7,8 @@ import { OrchestrationError } from '@/lib/core/orchestration/types' import { loadOrganizationSlackMemberApps, lockOrganizationSlackApp, + requireSearchBotMatchesMemberApp, + resolveOrganizationSlackMemberApp, } from '@/lib/credential-groups/organization-slack-app' import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks' import { @@ -173,7 +175,7 @@ export const configureSlackSearchInstallation = defineAuthorizedKnowledgeUseCase ? await tx.select().from(slackApp).where(eq(slackApp.id, current.slackAppId)).limit(1) : [] if (input.enabled && current.slackAppId) - await requireSlackSearchAppAvailable(current.slackAppId, context.organizationId) + await requireSlackSearchAppAvailable(current.slackAppId, context.organizationId, tx) if (current.slackAppId && !app) throw new Error('Slack app configuration is missing') const appRevision = app && secret ? (await resolveSlackAppCredentials(app)).revision : undefined @@ -239,6 +241,8 @@ export const configureSlackSearchInstallation = defineAuthorizedKnowledgeUseCase } if (!identity || !secret) throw new OrchestrationError('not_found', 'Slack Search installation not found') + const member = await resolveOrganizationSlackMemberApp(context.organizationId, tx) + requireSearchBotMatchesMemberApp(member.app, identity, app?.kind === 'shared') const [created] = await tx .insert(slackSearchInstallation) .values({ diff --git a/apps/sim/lib/knowledge/application/slack-search/setup.test.ts b/apps/sim/lib/knowledge/application/slack-search/setup.test.ts index 3fa9f732176..5fee1ad38cc 100644 --- a/apps/sim/lib/knowledge/application/slack-search/setup.test.ts +++ b/apps/sim/lib/knowledge/application/slack-search/setup.test.ts @@ -24,7 +24,7 @@ const hoisted = vi.hoisted(() => ({ shared: vi.fn(), revoke: vi.fn(), validateGrant: vi.fn(), - memberApps: vi.fn(), + memberApp: vi.fn(), adoptMemberApp: vi.fn(), insert: vi.fn(), update: vi.fn(), @@ -45,8 +45,11 @@ vi.mock('@/lib/internal/slack/oauth', () => ({ validateSlackBotAuthorization: hoisted.validateGrant, })) vi.mock('@/lib/credential-groups/service', () => credentialGroupsServiceMock) -vi.mock('@/lib/credential-groups/organization-slack-app', () => ({ - loadOrganizationSlackMemberApps: hoisted.memberApps, +vi.mock('@/lib/credential-groups/organization-slack-app', async (importOriginal) => ({ + requireSearchBotMatchesMemberApp: ( + await importOriginal() + ).requireSearchBotMatchesMemberApp, + resolveOrganizationSlackMemberApp: hoisted.memberApp, adoptOrganizationSlackMemberApp: hoisted.adoptMemberApp, lockOrganizationSlackApp: vi.fn(), })) @@ -100,7 +103,7 @@ beforeEach(() => { m.revoke.mockResolvedValue(undefined) m.validateGrant.mockReset() m.ensureGroup.mockResolvedValue({ id: 'accounts' }) - m.memberApps.mockReset().mockResolvedValue([]) + m.memberApp.mockReset().mockResolvedValue({ app: null, scopes: [] }) urlsMockFns.mockGetBaseUrl.mockReturnValue('https://sim.test') m.membership.mockResolvedValue([{ role: 'admin' }]) m.rows.mockReset().mockResolvedValue([]) @@ -311,9 +314,7 @@ describe('shared app completion', () => { } beforeEach(() => { - m.memberApps.mockResolvedValue([ - { configuration: { slack: { ...memberApp, scopes: ['im:history'] } } }, - ]) + m.memberApp.mockResolvedValue({ app: memberApp, scopes: ['im:history'] }) m.consume.mockResolvedValue({ ...attempt, sharedApp: { id: sharedApp.id, revision: sharedApp.revision }, @@ -428,17 +429,7 @@ describe('connect an app already installed through a Slack manifest', () => { }) it('rejects a token from a different app than member indexing', async () => { - m.memberApps.mockResolvedValue([ - { - configuration: { - slack: { - appId: 'AOTHER', - teamId: 'T1', - scopes: [], - }, - }, - }, - ]) + m.memberApp.mockResolvedValue({ app: { appId: 'AOTHER', teamId: 'T1' }, scopes: [] }) await expect(connect()).rejects.toThrow('same Slack app and workspace') expect(db.transaction).not.toHaveBeenCalled() }) diff --git a/apps/sim/lib/knowledge/application/slack-search/setup.ts b/apps/sim/lib/knowledge/application/slack-search/setup.ts index 3f77566eaae..92d6c84f94a 100644 --- a/apps/sim/lib/knowledge/application/slack-search/setup.ts +++ b/apps/sim/lib/knowledge/application/slack-search/setup.ts @@ -10,12 +10,12 @@ import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { getBaseUrl } from '@/lib/core/utils/urls' import { adoptOrganizationSlackMemberApp, - loadOrganizationSlackMemberApps, lockOrganizationSlackApp, + requireSearchBotMatchesMemberApp, + resolveOrganizationSlackMemberApp, } from '@/lib/credential-groups/organization-slack-app' import { configureSharedSlackMemberApp } from '@/lib/credential-groups/shared-slack-app' import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks' -import type { DbOrTx } from '@/lib/db/types' import { buildSlackAppCreationUrl } from '@/lib/integrations/slack-manifest' import { exchangeSlackBotAuthorization, @@ -63,36 +63,13 @@ interface CompleteInput { error?: string } -async function existingMemberApp(organizationId: string, executor: DbOrTx = db) { - const rows = await loadOrganizationSlackMemberApps(organizationId, executor) - const configurations = rows.flatMap((row) => - row.configuration.slack ? [row.configuration.slack] : [] - ) - const first = configurations[0] - if ( - first && - configurations.some( - (configuration) => - configuration.appId !== first.appId || configuration.teamId !== first.teamId - ) - ) - throw new OrchestrationError( - 'conflict', - 'Organization Slack member accounts reference multiple apps. Consolidate them before configuring Search.' - ) - return { - app: first ? { appId: first.appId, teamId: first.teamId } : null, - scopes: [...new Set(configurations.flatMap((configuration) => configuration.scopes))], - } -} - export const prepareSlackSearchSetup = defineAuthorizedKnowledgeUseCase({ operation: knowledgeOperations.prepareSlackInstallation, resolveContext: ({ input }: { input: PrepareInput }) => resolveKnowledgeOrganizationContext(input), async execute({ input, context }) { await requireOrganizationSearchAvailable(context.organizationId) - const member = await existingMemberApp(context.organizationId) + const member = await resolveOrganizationSlackMemberApp(context.organizationId) const manifest = createSlackSearchManifest( input.name, input.description, @@ -114,7 +91,7 @@ async function prepareInstallationAttempt( input: StartInput ): Promise { const origin = getBaseUrl() - const member = await existingMemberApp(input.organizationId) + const member = await resolveOrganizationSlackMemberApp(input.organizationId) const [installation] = input.installationId ? await db .select() @@ -279,15 +256,7 @@ async function saveInstallation( attempt.customInstallation.appId === identity.appId) ) throw new OrchestrationError('conflict', 'Install Sim Search in the same Slack workspace') - if ( - attempt.memberApp && - ((!attempt.sharedApp && attempt.memberApp.appId !== identity.appId) || - attempt.memberApp.teamId !== identity.teamId) - ) - throw new OrchestrationError( - 'conflict', - 'Install the same Slack app and workspace connected by your members' - ) + requireSearchBotMatchesMemberApp(attempt.memberApp, identity, Boolean(attempt.sharedApp)) const { encrypted: encryptedToken } = await encryptSecret( JSON.stringify({ type: SLACK_CUSTOM_BOT_SECRET_TYPE, @@ -440,7 +409,7 @@ async function saveInstallation( .values(appValues) .onConflictDoUpdate({ target: slackApp.id, set: appValues }) } - const member = await existingMemberApp(attempt.organizationId, tx) + const member = await resolveOrganizationSlackMemberApp(attempt.organizationId, tx) if ( member.app?.appId !== attempt.memberApp?.appId || member.app?.teamId !== attempt.memberApp?.teamId diff --git a/apps/sim/lib/knowledge/chunks/service.ts b/apps/sim/lib/knowledge/chunks/service.ts index b276f06398a..3f8b2bcc6f7 100644 --- a/apps/sim/lib/knowledge/chunks/service.ts +++ b/apps/sim/lib/knowledge/chunks/service.ts @@ -14,7 +14,6 @@ import { searchFilter, textKey, } from '@/lib/api/list-query' -import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks' import type { DurableSecretProvenance } from '@/lib/execution/durable-secret-provenance' import { knowledgeAccessCondition } from '@/lib/knowledge/access/predicate' import type { KnowledgeAccessScope } from '@/lib/knowledge/access/types' @@ -181,29 +180,10 @@ export async function queryChunks( } } -/** - * Create a new chunk for a document. - * - * Assigns `chunkIndex` as `max(chunkIndex) + 1` under a transactional - * `pg_advisory_xact_lock` keyed on the document, so concurrent calls for the - * same document serialize instead of computing the same index and colliding - * on the `(document_id, chunk_index)` unique constraint. A `SELECT ... FOR - * UPDATE` on the current max row doesn't prevent that collision, since the - * row it would lock is unrelated to the not-yet-inserted next row. A row - * lock on `document` instead of an advisory lock would also work, but would - * invert the embedding-before-document lock order every other chunk - * mutation path uses (see lock-order.test.ts) — the advisory lock is a - * separate namespace, so it can't deadlock against that convention. - * - * `pg_advisory_xact_lock` auto-releases at transaction end, so there's no - * session lock to leak onto a pooled connection, and `lock_timeout` bounds - * the wait (it raises SQLSTATE 55P03 instead of hanging a pooled connection) - * if a same-document holder is stuck. - */ +/** Assigns the next chunk index under the document row lock, shared by its metadata and chunk writers. */ export async function createChunk( knowledgeBaseId: string, documentId: string, - docTags: Record, chunkData: CreateChunkData, requestId: string, workspaceId?: string | null, @@ -240,10 +220,28 @@ export async function createChunk( await tx.execute( sql`select set_config('lock_timeout', ${`${KB_CHUNK_LOCK_TIMEOUT_MS}ms`}, true)` ) - await acquireAdvisoryXactLock(tx, 'kb_chunk_seq', `kb_chunk_seq:${documentId}`) const activeDocument = await tx - .select({ id: document.id }) + .select({ + id: document.id, + tag1: document.tag1, + tag2: document.tag2, + tag3: document.tag3, + tag4: document.tag4, + tag5: document.tag5, + tag6: document.tag6, + tag7: document.tag7, + number1: document.number1, + number2: document.number2, + number3: document.number3, + number4: document.number4, + number5: document.number5, + date1: document.date1, + date2: document.date2, + boolean1: document.boolean1, + boolean2: document.boolean2, + boolean3: document.boolean3, + }) .from(document) .innerJoin(knowledgeBase, eq(document.knowledgeBaseId, knowledgeBase.id)) .where( @@ -256,11 +254,14 @@ export async function createChunk( ) ) .limit(1) + .for('update', { of: document }) if (activeDocument.length === 0) { throw new Error('Document not found') } + const docTags = activeDocument[0] + const lastChunk = await tx .select({ chunkIndex: embedding.chunkIndex }) .from(embedding) @@ -284,23 +285,23 @@ export async function createChunk( embeddingModel: kbEmbeddingModel, startOffset: 0, // Manual chunks don't have document offsets endOffset: chunkData.content.length, - tag1: docTags.tag1 as string | null, - tag2: docTags.tag2 as string | null, - tag3: docTags.tag3 as string | null, - tag4: docTags.tag4 as string | null, - tag5: docTags.tag5 as string | null, - tag6: docTags.tag6 as string | null, - tag7: docTags.tag7 as string | null, - number1: docTags.number1 as number | null, - number2: docTags.number2 as number | null, - number3: docTags.number3 as number | null, - number4: docTags.number4 as number | null, - number5: docTags.number5 as number | null, - date1: docTags.date1 as Date | null, - date2: docTags.date2 as Date | null, - boolean1: docTags.boolean1 as boolean | null, - boolean2: docTags.boolean2 as boolean | null, - boolean3: docTags.boolean3 as boolean | null, + tag1: docTags.tag1, + tag2: docTags.tag2, + tag3: docTags.tag3, + tag4: docTags.tag4, + tag5: docTags.tag5, + tag6: docTags.tag6, + tag7: docTags.tag7, + number1: docTags.number1, + number2: docTags.number2, + number3: docTags.number3, + number4: docTags.number4, + number5: docTags.number5, + date1: docTags.date1, + date2: docTags.date2, + boolean1: docTags.boolean1, + boolean2: docTags.boolean2, + boolean3: docTags.boolean3, enabled: chunkData.enabled ?? true, createdAt: now, updatedAt: now, @@ -371,6 +372,11 @@ export async function batchChunkOperation( if (operation === 'delete') { // Handle batch delete with transaction for consistency await db.transaction(async (tx) => { + await tx + .select({ id: document.id }) + .from(document) + .where(eq(document.id, documentId)) + .for('update') // Get chunks to delete for statistics update const chunksToDelete = await tx .select({ @@ -509,6 +515,11 @@ export async function updateChunk( } const result = await db.transaction(async (tx) => { + await tx + .select({ id: document.id }) + .from(document) + .where(eq(document.id, preRead.documentId)) + .for('update') const currentChunk = await tx .select({ documentId: embedding.documentId, @@ -661,6 +672,11 @@ export async function deleteChunk( requestId: string ): Promise { await db.transaction(async (tx) => { + await tx + .select({ id: document.id }) + .from(document) + .where(eq(document.id, documentId)) + .for('update') // Get chunk data before deletion for statistics update const chunkToDelete = await tx .select({ diff --git a/apps/sim/lib/knowledge/connectors/sync-persistence.ts b/apps/sim/lib/knowledge/connectors/sync-persistence.ts index 2355a421be8..700ff71a025 100644 --- a/apps/sim/lib/knowledge/connectors/sync-persistence.ts +++ b/apps/sim/lib/knowledge/connectors/sync-persistence.ts @@ -28,7 +28,7 @@ import { leaseTransaction, type SyncWriteLease, } from '@/lib/knowledge/connectors/sync-lock' -import { MAX_DOCUMENT_INDEXED_TEXT_LENGTH } from '@/lib/knowledge/constants' +import { ALL_TAG_SLOTS, MAX_DOCUMENT_INDEXED_TEXT_LENGTH } from '@/lib/knowledge/constants' import type { DocumentData } from '@/lib/knowledge/documents/service' import { enqueueKnowledgeStorageCleanup } from '@/lib/knowledge/documents/storage-cleanup' import { @@ -345,6 +345,8 @@ async function isKnowledgeBaseActiveInTx( return rows.length > 0 } +type MappedDocumentTags = Partial> + /** * Resolves tag values from connector metadata using the connector's mapTags function. * Translates semantic keys returned by mapTags to actual DB slots using the @@ -354,7 +356,7 @@ export function resolveTagMapping( connectorType: string, metadata: Record, sourceConfig?: Record -): Partial | undefined { +): MappedDocumentTags | undefined { const config = CONNECTOR_REGISTRY[connectorType] if (!config?.mapTags || !metadata) return undefined @@ -362,7 +364,7 @@ export function resolveTagMapping( const mapping = sourceConfig?.tagSlotMapping as Record | undefined if (!mapping || !semanticTags) return undefined - const result: Partial = {} + const result: MappedDocumentTags = {} for (const [semanticKey, slot] of Object.entries(mapping)) { const value = semanticTags[semanticKey] ;(result as Record)[slot] = @@ -552,7 +554,7 @@ export async function persistSkippedDocuments( } /** Source-derived fields that can change without the indexed text changing. */ -export type SourceMetadataFields = Partial & { +export type SourceMetadataFields = MappedDocumentTags & { sourceUrl: string | null sourceModifiedAt: Date | null } @@ -607,6 +609,29 @@ export async function persistHashOnlyUpdates( .returning({ id: document.id }) if (persisted.length === 0) { missedExternalIds.push(update.externalId) + } else if (update.sourceMetadata) { + const tags = Object.fromEntries( + ALL_TAG_SLOTS.filter((slot) => update.sourceMetadata?.[slot] !== undefined).map( + (slot) => [slot, update.sourceMetadata?.[slot]] + ) + ) + const changed = ALL_TAG_SLOTS.filter((slot) => tags[slot] !== undefined) + if (changed.length > 0) { + await tx + .update(embedding) + .set(tags) + .where( + and( + eq(embedding.documentId, update.existingId), + or( + ...changed.map( + (slot) => + sql`${embedding[slot]} IS DISTINCT FROM ${sql.param(tags[slot], embedding[slot])}` + ) + ) + ) + ) + } } } }) diff --git a/apps/sim/lib/knowledge/documents/lock-order.test.ts b/apps/sim/lib/knowledge/documents/lock-order.test.ts deleted file mode 100644 index b622e0fa6fd..00000000000 --- a/apps/sim/lib/knowledge/documents/lock-order.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Lock-order regression guard: `updateDocument` must lock the document's - * embedding rows BEFORE the document row when cascading tag updates, matching - * the embedding → document order every chunk-mutation path uses - * (chunks/service.ts). The opposite order deadlocks a document tag edit against - * a concurrent chunk edit of the same document. - */ -import { document, embedding } from '@sim/db/schema' -import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' -import { beforeEach, describe, expect, it } from 'vitest' -import { updateDocument } from '@/lib/knowledge/documents/service' - -/** invocationCallOrder of the first `tx.update(table)` call. */ -function updateOrderForTable(table: unknown): number { - const { calls, invocationCallOrder } = dbChainMockFns.update.mock - for (let i = 0; i < calls.length; i++) { - if (calls[i][0] === table) return invocationCallOrder[i] - } - return -1 -} - -describe('updateDocument lock ordering', () => { - beforeEach(() => { - resetDbChainMock() - queueTableRows(document, [ - { id: 'doc-1', knowledgeBaseId: 'kb-1', secretProvenanceVersion: null }, - ]) - dbChainMockFns.returning.mockResolvedValue([ - { id: 'doc-1', knowledgeBaseId: 'kb-1', secretProvenanceVersion: null }, - ]) - }) - - it('updates embeddings before the document row when cascading tag changes', async () => { - await updateDocument('doc-1', { tag1: 'priority' }, 'req-1') - - const embeddingWriteOrder = updateOrderForTable(embedding) - const documentWriteOrder = updateOrderForTable(document) - - expect(embeddingWriteOrder).toBeGreaterThan(0) - expect(documentWriteOrder).toBeGreaterThan(0) - expect(embeddingWriteOrder).toBeLessThan(documentWriteOrder) - }) -}) diff --git a/apps/sim/lib/knowledge/documents/service.ts b/apps/sim/lib/knowledge/documents/service.ts index 017aca1c451..a60e2b43d94 100644 --- a/apps/sim/lib/knowledge/documents/service.ts +++ b/apps/sim/lib/knowledge/documents/service.ts @@ -89,6 +89,7 @@ import { requiresConnectorIndexing, } from '@/lib/knowledge/connectors/indexing-policy' import { assertSyncLeaseHeldInTx, type SyncWriteLease } from '@/lib/knowledge/connectors/sync-lock' +import { ALL_TAG_SLOTS } from '@/lib/knowledge/constants' import { documentConnectorIsActive } from '@/lib/knowledge/documents/connector-lifecycle' import { createEmbeddingCheckpoints } from '@/lib/knowledge/documents/embedding-checkpoints' import { @@ -2066,7 +2067,6 @@ export async function processDocumentAsync( `Embedding generation returned ${embeddings.length} vectors for ${processed.chunks.length} chunks` ) } - const documentTags = ctx logger.info( `[${documentId}] Embeddings generated, creating embedding records with tags` @@ -2082,40 +2082,6 @@ export async function processDocumentAsync( : EXACT_EMPTY_DURABLE_SECRET_PROVENANCE : undefined ) - const embeddingRecords = processed.chunks.map((chunk, chunkIndex) => ({ - id: generateId(), - knowledgeBaseId, - documentId, - chunkIndex, - chunkHash: sha256Hex(chunk.text), - content: chunk.text, - secretProvenanceVersion: chunkProvenances[chunkIndex] ? 1 : null, - contentLength: chunk.text.length, - tokenCount: chunkTokenCounts[chunkIndex], - ...embeddingVectorValues(kbEmbedding.dimensions, embeddings[chunkIndex]), - embeddingModel: kbEmbeddingModel, - startOffset: chunk.metadata.startIndex, - endOffset: chunk.metadata.endIndex, - tag1: documentTags.tag1, - tag2: documentTags.tag2, - tag3: documentTags.tag3, - tag4: documentTags.tag4, - tag5: documentTags.tag5, - tag6: documentTags.tag6, - tag7: documentTags.tag7, - number1: documentTags.number1, - number2: documentTags.number2, - number3: documentTags.number3, - number4: documentTags.number4, - number5: documentTags.number5, - date1: documentTags.date1, - date2: documentTags.date2, - boolean1: documentTags.boolean1, - boolean2: documentTags.boolean2, - boolean3: documentTags.boolean3, - createdAt: now, - updatedAt: now, - })) signal.throwIfAborted() /** @@ -2149,7 +2115,26 @@ export async function processDocumentAsync( * through the embedding foreign key and projection triggers. */ const activeDocument = await tx - .select({ id: document.id }) + .select({ + id: document.id, + tag1: document.tag1, + tag2: document.tag2, + tag3: document.tag3, + tag4: document.tag4, + tag5: document.tag5, + tag6: document.tag6, + tag7: document.tag7, + number1: document.number1, + number2: document.number2, + number3: document.number3, + number4: document.number4, + number5: document.number5, + date1: document.date1, + date2: document.date2, + boolean1: document.boolean1, + boolean2: document.boolean2, + boolean3: document.boolean3, + }) .from(document) .where( and( @@ -2169,6 +2154,42 @@ export async function processDocumentAsync( return false } + const documentTags = activeDocument[0] + const embeddingRecords = processed.chunks.map((chunk, chunkIndex) => ({ + id: generateId(), + knowledgeBaseId, + documentId, + chunkIndex, + chunkHash: sha256Hex(chunk.text), + content: chunk.text, + secretProvenanceVersion: chunkProvenances[chunkIndex] ? 1 : null, + contentLength: chunk.text.length, + tokenCount: chunkTokenCounts[chunkIndex], + ...embeddingVectorValues(kbEmbedding.dimensions, embeddings[chunkIndex]), + embeddingModel: kbEmbeddingModel, + startOffset: chunk.metadata.startIndex, + endOffset: chunk.metadata.endIndex, + tag1: documentTags.tag1, + tag2: documentTags.tag2, + tag3: documentTags.tag3, + tag4: documentTags.tag4, + tag5: documentTags.tag5, + tag6: documentTags.tag6, + tag7: documentTags.tag7, + number1: documentTags.number1, + number2: documentTags.number2, + number3: documentTags.number3, + number4: documentTags.number4, + number5: documentTags.number5, + date1: documentTags.date1, + date2: documentTags.date2, + boolean1: documentTags.boolean1, + boolean2: documentTags.boolean2, + boolean3: documentTags.boolean3, + createdAt: now, + updatedAt: now, + })) + /** * A pass that indexes nothing (a decorative image) still replaces what an * earlier pass indexed, or the document would stay searchable by stale text. @@ -4008,25 +4029,6 @@ export async function updateDocument( boolean2: boolean | null boolean3: boolean | null }> = {} - const ALL_TAG_SLOTS = [ - 'tag1', - 'tag2', - 'tag3', - 'tag4', - 'tag5', - 'tag6', - 'tag7', - 'number1', - 'number2', - 'number3', - 'number4', - 'number5', - 'date1', - 'date2', - 'boolean1', - 'boolean2', - 'boolean3', - ] as const type TagSlot = (typeof ALL_TAG_SLOTS)[number] if (updateData.filename !== undefined) dbUpdateData.filename = updateData.filename @@ -4073,6 +4075,14 @@ export async function updateDocument( }) const doc = await db.transaction(async (tx) => { + const [current] = await tx + .select() + .from(document) + .where(eq(document.id, documentId)) + .limit(1) + .for('update') + if (!current) return undefined + const hasTagUpdates = ALL_TAG_SLOTS.some((field) => typedUpdateData[field] !== undefined) if (hasTagUpdates) { @@ -4091,14 +4101,6 @@ export async function updateDocument( .where(eq(embedding.documentId, documentId)) } - const [current] = await tx - .select() - .from(document) - .where(eq(document.id, documentId)) - .limit(1) - .for('update') - if (!current) return undefined - const [sidecar] = current.secretProvenanceVersion === 1 ? await tx @@ -4484,29 +4486,24 @@ async function hardDeleteDocumentBatch( * embedding delete and the document delete are scoped to this re-verified * ID set rather than the stale `existingIds`. */ - const stillTargetedIds = - scopedConnectorId || scopedKnowledgeBaseId || accessCondition - ? ( - await tx - .select({ id: document.id }) - .from(document) - .where( - and( - inArray(document.id, existingIds), - scopedConnectorId ? eq(document.connectorId, scopedConnectorId) : undefined, - scopedKnowledgeBaseId - ? eq(document.knowledgeBaseId, scopedKnowledgeBaseId) - : undefined, - requireEligibleDocument ? eq(document.userExcluded, false) : undefined, - requireEligibleDocument ? isNull(document.archivedAt) : undefined, - requireVisibleDocument ? isNull(document.deletedAt) : undefined, - accessCondition - ) - ) - .orderBy(asc(document.id)) - .for('update') - ).map((d) => d.id) - : existingIds + const stillTargetedIds = ( + await tx + .select({ id: document.id }) + .from(document) + .where( + and( + inArray(document.id, existingIds), + scopedConnectorId ? eq(document.connectorId, scopedConnectorId) : undefined, + scopedKnowledgeBaseId ? eq(document.knowledgeBaseId, scopedKnowledgeBaseId) : undefined, + requireEligibleDocument ? eq(document.userExcluded, false) : undefined, + requireEligibleDocument ? isNull(document.archivedAt) : undefined, + requireVisibleDocument ? isNull(document.deletedAt) : undefined, + accessCondition + ) + ) + .orderBy(asc(document.id)) + .for('update') + ).map((d) => d.id) await tx.delete(embedding).where(inArray(embedding.documentId, stillTargetedIds)) const deletedRows = await tx diff --git a/apps/sim/lib/knowledge/tags/secret-provenance-delete.test.ts b/apps/sim/lib/knowledge/tags/secret-provenance-delete.test.ts index b9d99d731f7..151e46d66e5 100644 --- a/apps/sim/lib/knowledge/tags/secret-provenance-delete.test.ts +++ b/apps/sim/lib/knowledge/tags/secret-provenance-delete.test.ts @@ -2,7 +2,6 @@ import { document, knowledgeBase, knowledgeBaseTagDefinitions } from '@sim/db/sc import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' import { beforeEach, describe, expect, it } from 'vitest' import { - deleteAllTagDefinitions, deleteTagDefinition, KnowledgeTagProvenanceConflictError, } from '@/lib/knowledge/tags/service' @@ -36,19 +35,4 @@ describe('knowledge tag deletion provenance', () => { expect(dbChainMockFns.update).not.toHaveBeenCalled() expect(dbChainMockFns.delete).not.toHaveBeenCalled() }) - - it('clears every bounded tag slot through the same guarded mutation path', async () => { - queueTableRows(knowledgeBase, [{ id: KNOWLEDGE_BASE_ID }]) - queueTableRows(knowledgeBaseTagDefinitions, [ - { id: 'tag-definition-1', tagSlot: 'tag1' }, - { id: 'tag-definition-2', tagSlot: 'number1' }, - ]) - queueTableRows(document, []) - - await expect(deleteAllTagDefinitions(KNOWLEDGE_BASE_ID, 'request-1')).resolves.toBe(2) - - expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(1) - expect(dbChainMockFns.execute).toHaveBeenCalledTimes(1) - expect(dbChainMockFns.set).toHaveBeenCalledWith({ tag1: null, number1: null }) - }) }) diff --git a/apps/sim/lib/knowledge/tags/service.test.ts b/apps/sim/lib/knowledge/tags/service.test.ts index 068428a5de5..2d6c9c227c7 100644 --- a/apps/sim/lib/knowledge/tags/service.test.ts +++ b/apps/sim/lib/knowledge/tags/service.test.ts @@ -182,19 +182,6 @@ describe('createOrUpdateTagDefinitionsBulk', () => { expect(result.errors).toEqual(['Display name "CLITEST-CAT" already exists']) expect(dbChainMockFns.insert).not.toHaveBeenCalled() }) - - it('reads the snapshot it compares against under the knowledge base row lock', async () => { - queueTableRows(knowledgeBaseTagDefinitions, []) - - await createOrUpdateTagDefinitionsBulk( - 'kb-1', - { definitions: [{ tagSlot: 'tag1', displayName: 'clitest-locked', fieldType: 'text' }] }, - 'request-10' - ) - - expect(dbChainMockFns.transaction).toHaveBeenCalled() - expect(dbChainMockFns.for).toHaveBeenCalledWith('update') - }) }) describe('getTagUsageStats', () => { diff --git a/apps/sim/lib/knowledge/tags/service.ts b/apps/sim/lib/knowledge/tags/service.ts index 6e7fb2381cb..03091a79567 100644 --- a/apps/sim/lib/knowledge/tags/service.ts +++ b/apps/sim/lib/knowledge/tags/service.ts @@ -73,7 +73,7 @@ async function lockKnowledgeBaseForTagMutation( .from(knowledgeBase) .where(eq(knowledgeBase.id, knowledgeBaseId)) .limit(1) - .for('update') + .for('no key update') } /** Prevents tag cleanup from demoting nonempty or unverifiable provenance to legacy state. */ @@ -112,6 +112,15 @@ async function clearTagSlotsInTx( const clearedTagValues: ClearedTagValues = {} for (const tagSlot of tagSlots) clearedTagValues[tagSlot] = null + await tx.execute(sql` + SELECT count(*) FROM ( + SELECT ${document.id} FROM ${document} + WHERE ${document.knowledgeBaseId} = ${knowledgeBaseId} + ORDER BY ${document.id} + FOR UPDATE + ) AS locked_documents + `) + await assertKnowledgeBaseTagsCanBeClearedInTx(tx, knowledgeBaseId) await tx diff --git a/apps/sim/lib/media/falai-audio.ts b/apps/sim/lib/media/falai-audio.ts index 6af06843c80..f55b27d633d 100644 --- a/apps/sim/lib/media/falai-audio.ts +++ b/apps/sim/lib/media/falai-audio.ts @@ -1,5 +1,12 @@ -import { downloadFalMedia, extractFalMediaUrl, getFalApiKey, runFalQueue } from '@/lib/media/falai' -import { type FalAICostMetadata, getFalAICostMetadata } from '@/lib/tools/falai-pricing' +import { + downloadFalMedia, + extractFalMediaUrl, + type FalCostRecorder, + getFalApiKey, + recordFalCost, + runFalQueue, +} from '@/lib/media/falai' +import type { FalAICostMetadata } from '@/lib/tools/falai-pricing' export type AudioType = 'speech' | 'music' | 'sfx' @@ -26,6 +33,10 @@ export interface GenerateFalAudioParams { instrumental?: boolean /** When set, clones the voice from this reference sample (data URI) via a zero-shot clone model. */ voiceSampleDataUri?: string + /** Ends the generation: the queued request is cancelled and every wait stops. */ + signal?: AbortSignal + /** Bills the generation once fal completes it, even if a Stop then ends the download. */ + onCost: FalCostRecorder } export interface GeneratedAudio { @@ -94,11 +105,11 @@ export async function generateFalAudio(params: GenerateFalAudioParams): Promise< ref_audio_url: params.voiceSampleDataUri, model_type: 'F5-TTS', } - const { requestId, data } = await runFalQueue(model, input, apiKey) + const { requestId, data } = await runFalQueue(model, input, apiKey, params.signal) + const cost = await recordFalCost(apiKey, model, requestId, params.onCost) const url = extractFalMediaUrl(data, ['audio', 'audio_url', 'audio_file', 'output']) if (!url) throw new Error('No audio URL in Fal.ai clone response') - const { buffer, contentType } = await downloadFalMedia(url) - const cost = await getFalAICostMetadata({ apiKey, endpointId: model, requestId }) + const { buffer, contentType } = await downloadFalMedia(url, params.signal) return { buffer, contentType: contentType.startsWith('audio/') ? contentType : 'audio/mpeg', @@ -113,12 +124,12 @@ export async function generateFalAudio(params: GenerateFalAudioParams): Promise< const input = buildInput(type, params, model) // For fal audio models the model ID is the queue endpoint. - const { requestId, data } = await runFalQueue(model, input, apiKey) + const { requestId, data } = await runFalQueue(model, input, apiKey, params.signal) + const cost = await recordFalCost(apiKey, model, requestId, params.onCost) const url = extractFalMediaUrl(data, ['audio', 'audio_url', 'audio_file', 'output']) if (!url) throw new Error('No audio URL in Fal.ai response') - const { buffer, contentType } = await downloadFalMedia(url) - const cost = await getFalAICostMetadata({ apiKey, endpointId: model, requestId }) + const { buffer, contentType } = await downloadFalMedia(url, params.signal) return { buffer, diff --git a/apps/sim/lib/media/falai-video.ts b/apps/sim/lib/media/falai-video.ts index b0e1ff86781..cdea37ea4cc 100644 --- a/apps/sim/lib/media/falai-video.ts +++ b/apps/sim/lib/media/falai-video.ts @@ -2,11 +2,13 @@ import { isRecordLike } from '@sim/utils/object' import { downloadFalMedia, extractFalMediaUrl, + type FalCostRecorder, getFalApiKey, getNumberProp, + recordFalCost, runFalQueue, } from '@/lib/media/falai' -import { type FalAICostMetadata, getFalAICostMetadata } from '@/lib/tools/falai-pricing' +import type { FalAICostMetadata } from '@/lib/tools/falai-pricing' type DurationFormat = 'number' | 'seconds' | 'string' @@ -110,6 +112,10 @@ export interface GenerateFalVideoParams { promptOptimizer?: boolean /** Optional start-frame image as a data URI; when set, routes to the model's image-to-video endpoint. */ imageDataUri?: string + /** Ends the generation: the queued request is cancelled and every wait stops. */ + signal?: AbortSignal + /** Bills the generation once fal completes it, even if a Stop then ends the download. */ + onCost: FalCostRecorder } export interface GeneratedVideo { @@ -171,13 +177,13 @@ export async function generateFalVideo(params: GenerateFalVideoParams): Promise< input.prompt_optimizer = params.promptOptimizer } - const { requestId, data } = await runFalQueue(endpoint, input, apiKey) + const { requestId, data } = await runFalQueue(endpoint, input, apiKey, params.signal) + const cost = await recordFalCost(apiKey, endpoint, requestId, params.onCost) const url = extractFalMediaUrl(data, ['video', 'output']) if (!url) throw new Error('No video URL in Fal.ai response') const videoNode = isRecordLike(data.video) ? data.video : undefined - const { buffer, contentType } = await downloadFalMedia(url) - const cost = await getFalAICostMetadata({ apiKey, endpointId: endpoint, requestId }) + const { buffer, contentType } = await downloadFalMedia(url, params.signal) return { buffer, diff --git a/apps/sim/lib/media/falai.test.ts b/apps/sim/lib/media/falai.test.ts new file mode 100644 index 00000000000..5407736b4d0 --- /dev/null +++ b/apps/sim/lib/media/falai.test.ts @@ -0,0 +1,401 @@ +import { createDeferred } from '@sim/testing/helpers/deferred' +import { jsonResponse } from '@sim/testing/helpers/http' +import { + inputValidationMock, + inputValidationMockFns, +} from '@sim/testing/mocks/input-validation.mock' +import { getMockLogger } from '@sim/testing/mocks/logger.mock' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) + +import { downloadFalMedia, type FalCostRecorder, runFalQueue } from '@/lib/media/falai' +import { generateFalAudio } from '@/lib/media/falai-audio' +import { generateFalVideo } from '@/lib/media/falai-video' + +const CANCEL_URL = 'https://queue.fal.run/fal-ai/model/requests/req-1/cancel' +const STATUS_URL = 'https://queue.fal.run/fal-ai/model/requests/req-1/status' + +interface FalCall { + url: string + method: string + headers: Record +} + +/** + * A fal queue whose request is accepted and then never finishes: every status poll reports + * IN_PROGRESS, or (`statusHangs`) never answers at all until its signal aborts. + */ +function stubStuckFalQueue(options: { statusHangs?: boolean; cancelStatus?: number } = {}) { + const calls: FalCall[] = [] + vi.stubGlobal( + 'fetch', + vi.fn(async (url: string, init: RequestInit = {}) => { + calls.push({ + url, + method: init.method ?? 'GET', + headers: (init.headers ?? {}) as Record, + }) + if (init.method === 'POST') + return jsonResponse({ + request_id: 'req-1', + status_url: STATUS_URL, + cancel_url: CANCEL_URL, + }) + if (init.method === 'PUT') + return jsonResponse({ status: 'CANCELLATION_REQUESTED' }, options.cancelStatus ?? 202) + if (options.statusHangs) + return new Promise((_resolve, reject) => { + init.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true }) + }) + return jsonResponse({ status: 'IN_PROGRESS' }) + }) + ) + const statusPolls = () => calls.filter((call) => call.url === STATUS_URL).length + const cancels = () => calls.filter((call) => call.method === 'PUT') + return { calls, statusPolls, cancels } +} + +describe('runFalQueue on a request that never finishes', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.useRealTimers() + }) + + it('stops polling and cancels the request once its caller gives up', async () => { + const fal = stubStuckFalQueue() + const caller = new AbortController() + const generation = runFalQueue('fal-ai/model', { prompt: 'p' }, 'key', caller.signal) + const outcome = generation.then( + () => 'resolved', + () => 'rejected' + ) + + await vi.advanceTimersByTimeAsync(9_500) + expect(fal.statusPolls()).toBe(3) + caller.abort(new Error('watchdog')) + await vi.advanceTimersByTimeAsync(0) + + expect(await outcome).toBe('rejected') + expect(fal.cancels()).toEqual([expect.objectContaining({ url: CANCEL_URL })]) + await vi.advanceTimersByTimeAsync(60_000) + expect(fal.statusPolls()).toBe(3) + }) + + it('ends a poll that never answers when its caller gives up', async () => { + const fal = stubStuckFalQueue({ statusHangs: true }) + const caller = new AbortController() + const generation = runFalQueue('fal-ai/model', { prompt: 'p' }, 'key', caller.signal) + const outcome = generation.then( + () => 'resolved', + () => 'rejected' + ) + + await vi.advanceTimersByTimeAsync(3_000) + expect(fal.statusPolls()).toBe(1) + caller.abort(new Error('watchdog')) + await vi.advanceTimersByTimeAsync(0) + + expect(await outcome).toBe('rejected') + expect(fal.cancels()).toHaveLength(1) + }) + + it('fails and cancels after its own ceiling even when nothing aborts it', async () => { + const fal = stubStuckFalQueue() + const generation = runFalQueue('fal-ai/model', { prompt: 'p' }, 'key') + const outcome = generation.then( + () => 'resolved', + (error: unknown) => String(error) + ) + + await vi.advanceTimersByTimeAsync(30 * 60 * 1000) + + expect(await outcome).toContain('Fal.ai generation timed out') + expect(fal.cancels()).toHaveLength(1) + }) + + it('warns when fal refuses to cancel a request', async () => { + const fal = stubStuckFalQueue({ cancelStatus: 500 }) + const caller = new AbortController() + const generation = runFalQueue('fal-ai/model', { prompt: 'p' }, 'key', caller.signal).catch( + () => undefined + ) + await vi.advanceTimersByTimeAsync(3_500) + caller.abort() + await generation + + expect(fal.cancels()).toHaveLength(1) + expect(getMockLogger('FalMediaClient').warn).toHaveBeenCalledWith( + 'Fal.ai request cancellation failed', + expect.objectContaining({ requestId: 'req-1' }) + ) + }) + + it('submits nothing for a caller that already gave up', async () => { + const fal = stubStuckFalQueue() + + await expect( + runFalQueue('fal-ai/model', { prompt: 'p' }, 'key', AbortSignal.abort()) + ).rejects.toThrow() + expect(fal.calls).toEqual([]) + }) + + it('cancels a request its caller gave up on while submitting it', async () => { + const calls: string[] = [] + let accept: (response: Response) => void = () => {} + vi.stubGlobal( + 'fetch', + vi.fn((url: string, init: RequestInit = {}) => { + calls.push(`${init.method ?? 'GET'} ${url}`) + if (init.method === 'POST') + return new Promise((resolve, reject) => { + accept = resolve + init.signal?.addEventListener('abort', () => reject(init.signal?.reason), { + once: true, + }) + }) + return Promise.resolve(jsonResponse({ status: 'CANCELLATION_REQUESTED' }, 202)) + }) + ) + const caller = new AbortController() + const generation = runFalQueue('fal-ai/model', { prompt: 'p' }, 'key', caller.signal).catch( + () => 'rejected' + ) + + await vi.advanceTimersByTimeAsync(0) + caller.abort(new Error('watchdog')) + accept(jsonResponse({ request_id: 'req-1', cancel_url: CANCEL_URL })) + + expect(await generation).toBe('rejected') + expect(calls).toContain(`PUT ${CANCEL_URL}`) + }) + + it('asks fal never to start the request past that ceiling', async () => { + const fal = stubStuckFalQueue() + const caller = new AbortController() + const generation = runFalQueue('fal-ai/model', { prompt: 'p' }, 'key', caller.signal).catch( + () => undefined + ) + await vi.advanceTimersByTimeAsync(0) + caller.abort() + await generation + + expect(fal.calls[0]).toMatchObject({ + method: 'POST', + headers: expect.objectContaining({ 'X-Fal-Request-Timeout': '1800' }), + }) + }) +}) + +describe('downloadFalMedia', () => { + it('returns nothing for a caller that already gave up, even inline media', async () => { + await expect( + downloadFalMedia('data:audio/mpeg;base64,AAAA', AbortSignal.abort()) + ).rejects.toThrow() + }) + + it('ends a download still resolving its host once its caller gives up', async () => { + inputValidationMockFns.mockValidateUrlWithDNS.mockImplementationOnce( + (_url: string, _name: string, _profile: string, options?: { signal?: AbortSignal }) => + new Promise((_resolve, reject) => { + options?.signal?.addEventListener('abort', () => reject(options.signal?.reason), { + once: true, + }) + }) + ) + const caller = new AbortController() + const download = downloadFalMedia('https://media.example/out.mp3', caller.signal) + + caller.abort(new Error('watchdog')) + + await expect(download).rejects.toThrow('watchdog') + }) +}) + +const MEDIA_URL = 'https://media.example/out.bin' +const BILLED_NANO_USD = 40_000_000 + +/** + * A fal queue whose request completes on the first poll. The billing-event lookup answers at + * once, answers only once `answered` settles (whatever its caller does), or (`hangs`) never. + */ +function stubCompletedFalQueue( + billing: { answered?: Promise; hangs?: boolean } = {}, + result: Record = { audio: { url: MEDIA_URL }, video: { url: MEDIA_URL } } +) { + const billingLookups: string[] = [] + vi.stubGlobal( + 'fetch', + vi.fn(async (input: string | URL, init: RequestInit = {}) => { + const url = String(input) + if (url === STATUS_URL) return jsonResponse({ status: 'COMPLETED' }) + if (url.endsWith('/requests/req-1/response')) return jsonResponse(result) + if (!url.startsWith('https://api.fal.ai/v1/models/billing-events')) + return jsonResponse({ request_id: 'req-1', status_url: STATUS_URL }) + billingLookups.push(url) + if (billing.hangs) + return new Promise((_resolve, reject) => { + init.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true }) + }) + await billing.answered + return jsonResponse({ + billing_events: [ + { + request_id: 'req-1', + endpoint_id: 'fal-ai/model', + cost_estimate_nano_usd: BILLED_NANO_USD, + }, + ], + }) + }) + ) + return { billingLookups } +} + +/** The generated media's host resolves; the download then answers with `download`. */ +function stubMediaHost(download: (options: { signal?: AbortSignal }) => Promise) { + inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValueOnce({ + isValid: true, + resolvedIP: '203.0.113.1', + }) + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockImplementationOnce( + (_url: string, _ip: string, options: { signal?: AbortSignal }) => download(options) + ) +} + +const downloadAudio = async () => + new Response('audio', { headers: { 'content-type': 'audio/mpeg' } }) + +const startAudio = (onCost: FalCostRecorder, signal?: AbortSignal) => + generateFalAudio({ prompt: 'p', model: 'fal-ai/model', signal, onCost }) + +const generators = [ + ['audio', startAudio], + [ + 'video', + (onCost: FalCostRecorder, signal?: AbortSignal) => + generateFalVideo({ prompt: 'p', signal, onCost }), + ], +] as const + +describe('billing a fal generation', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.stubEnv('FALAI_API_KEY', 'key') + }) + afterEach(() => { + vi.useRealTimers() + inputValidationMockFns.mockValidateUrlWithDNS.mockReset() + inputValidationMockFns.mockSecureFetchWithPinnedIP.mockReset() + }) + + it.each(generators)( + 'bills a completed %s generation once when a Stop ends its download', + async (_kind, generate) => { + stubCompletedFalQueue() + stubMediaHost( + ({ signal }) => + new Promise((_resolve, reject) => { + signal?.addEventListener('abort', () => reject(signal.reason), { once: true }) + }) + ) + const onCost = vi.fn().mockResolvedValue(undefined) + const caller = new AbortController() + const generation = generate(onCost, caller.signal).catch((error: unknown) => error) + + await vi.advanceTimersByTimeAsync(3_000) + expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).toHaveBeenCalled() + caller.abort(new Error('watchdog')) + + expect(await generation).toEqual(new Error('watchdog')) + expect(onCost).toHaveBeenCalledTimes(1) + expect(onCost).toHaveBeenCalledWith( + expect.objectContaining({ source: 'billing_events', costDollars: 0.04 }) + ) + } + ) + + it.each(generators)( + 'bills a completed %s generation once when its result has no media URL', + async (_kind, generate) => { + stubCompletedFalQueue({}, { unexpected: true }) + const onCost = vi.fn().mockResolvedValue(undefined) + const generation = generate(onCost).catch((error: unknown) => String(error)) + + await vi.advanceTimersByTimeAsync(3_000) + + expect(await generation).toMatch(/No (audio|video) URL/) + expect(onCost).toHaveBeenCalledTimes(1) + expect(onCost).toHaveBeenCalledWith(expect.objectContaining({ source: 'billing_events' })) + } + ) + + it('finishes the cost lookup of a completed generation a Stop lands in, then stops', async () => { + const billingAnswer = createDeferred() + const fal = stubCompletedFalQueue({ answered: billingAnswer.promise }) + stubMediaHost(downloadAudio) + const onCost = vi.fn().mockResolvedValue(undefined) + const caller = new AbortController() + const generation = startAudio(onCost, caller.signal).catch((error: unknown) => error) + + await vi.advanceTimersByTimeAsync(3_000) + expect(fal.billingLookups).toHaveLength(1) + caller.abort(new Error('watchdog')) + billingAnswer.resolve() + + expect(await generation).toEqual(new Error('watchdog')) + expect(onCost).toHaveBeenCalledTimes(1) + expect(onCost).toHaveBeenCalledWith( + expect.objectContaining({ source: 'billing_events', costDollars: 0.04 }) + ) + expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() + }) + + it('bills the fallback floor once when the cost lookup outlasts its own bound', async () => { + stubCompletedFalQueue({ hangs: true }) + stubMediaHost(downloadAudio) + const onCost = vi.fn().mockResolvedValue(undefined) + const caller = new AbortController() + const generation = startAudio(onCost, caller.signal).catch((error: unknown) => error) + + await vi.advanceTimersByTimeAsync(3_000) + caller.abort(new Error('watchdog')) + await vi.advanceTimersByTimeAsync(9_999) + expect(onCost).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(1) + + expect(await generation).toEqual(new Error('watchdog')) + expect(onCost).toHaveBeenCalledTimes(1) + expect(onCost).toHaveBeenCalledWith(expect.objectContaining({ source: 'fallback_floor' })) + }) + + it('bills a generation that completes and downloads exactly once', async () => { + stubCompletedFalQueue() + stubMediaHost(downloadAudio) + const onCost = vi.fn().mockResolvedValue(undefined) + const generation = startAudio(onCost) + + await vi.advanceTimersByTimeAsync(3_000) + const audio = await generation + + expect(audio.buffer.toString()).toBe('audio') + expect(onCost).toHaveBeenCalledTimes(1) + expect(onCost).toHaveBeenCalledWith(audio.cost) + }) + + it('bills nothing for a generation stopped before fal completes it', async () => { + const fal = stubStuckFalQueue() + const onCost = vi.fn().mockResolvedValue(undefined) + const caller = new AbortController() + const generation = startAudio(onCost, caller.signal).catch((error: unknown) => error) + + await vi.advanceTimersByTimeAsync(3_500) + caller.abort(new Error('watchdog')) + + expect(await generation).toEqual(new Error('watchdog')) + expect(fal.cancels()).toHaveLength(1) + expect(onCost).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/media/falai.ts b/apps/sim/lib/media/falai.ts index db4dc0a6152..40db1bd5f6e 100644 --- a/apps/sim/lib/media/falai.ts +++ b/apps/sim/lib/media/falai.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' -import { sleep } from '@sim/utils/helpers' +import { getErrorMessage } from '@sim/utils/errors' +import { interruptibleSleep } from '@sim/utils/helpers' import { isRecordLike } from '@sim/utils/object' -import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { secureFetchWithPinnedIP, validateUrlWithDNS, @@ -13,6 +13,7 @@ import { readResponseTextWithLimit, readResponseToBufferWithLimit, } from '@/lib/core/utils/stream-limits' +import { type FalAICostMetadata, getFalAICostMetadata } from '@/lib/tools/falai-pricing' const logger = createLogger('FalMediaClient') @@ -20,6 +21,17 @@ const logger = createLogger('FalMediaClient') export const MAX_MEDIA_BYTES = 250 * 1024 * 1024 const MAX_MEDIA_JSON_BYTES = 4 * 1024 * 1024 const POLL_INTERVAL_MS = 3000 +/** + * Longest a queued generation may take, whatever the caller allows: past it the request is + * cancelled and fails. Also sent as fal's start deadline, so a request still queued then never runs. + */ +const FAL_QUEUE_MAX_MS = 30 * 60 * 1000 +const FAL_CANCEL_TIMEOUT_MS = 5000 +/** + * The submit is never cut short by the caller: an interrupted POST fal may already have accepted + * would leave a generation running that nothing can cancel. It has its own bound instead. + */ +const FAL_SUBMIT_TIMEOUT_MS = 60_000 /** * Resolves a hosted Fal.ai API key from the numbered env pool @@ -61,7 +73,11 @@ export function getNumberProp( return typeof value === 'number' ? value : undefined } -function falQueueUrl(endpoint: string, requestId: string, path: 'status' | 'response'): string { +function falQueueUrl( + endpoint: string, + requestId: string, + path: 'status' | 'response' | 'cancel' +): string { return `https://queue.fal.run/${endpoint}/requests/${requestId}/${path}` } @@ -78,17 +94,45 @@ export interface FalQueueResult { /** * Submit input to a Fal.ai queue endpoint, poll to completion, and return the - * result JSON. Shared by the video and audio generators. + * result JSON. Shared by the video and audio generators. Every request and wait ends when + * `signal` aborts or {@link FAL_QUEUE_MAX_MS} passes; a submitted request is then cancelled. */ export async function runFalQueue( endpoint: string, input: Record, - apiKey: string + apiKey: string, + signal?: AbortSignal +): Promise { + const ceiling = new AbortController() + const ceilingTimer = setTimeout( + () => ceiling.abort(new Error('Fal.ai generation timed out')), + FAL_QUEUE_MAX_MS + ) + const queueSignal = signal ? AbortSignal.any([signal, ceiling.signal]) : ceiling.signal + try { + return await submitAndPollFalQueue(endpoint, input, apiKey, queueSignal) + } finally { + clearTimeout(ceilingTimer) + } +} + +async function submitAndPollFalQueue( + endpoint: string, + input: Record, + apiKey: string, + queueSignal: AbortSignal ): Promise { + // Only a submit already under way outlives its caller; a stopped call never starts one. + queueSignal.throwIfAborted() const createResponse = await fetch(`https://queue.fal.run/${endpoint}`, { method: 'POST', - headers: { Authorization: `Key ${apiKey}`, 'Content-Type': 'application/json' }, + headers: { + Authorization: `Key ${apiKey}`, + 'Content-Type': 'application/json', + 'X-Fal-Request-Timeout': String(FAL_QUEUE_MAX_MS / 1000), + }, body: JSON.stringify(input), + signal: AbortSignal.timeout(FAL_SUBMIT_TIMEOUT_MS), }) if (!createResponse.ok) { const err = await readResponseTextWithLimit(createResponse, { @@ -107,16 +151,36 @@ export async function runFalQueue( const requestId = getStringProp(createData, 'request_id') if (!requestId) throw new Error('Fal.ai queue response missing request_id') + const cancelUrl = + getStringProp(createData, 'cancel_url') || falQueueUrl(endpoint, requestId, 'cancel') + try { + return await pollFalQueue(endpoint, requestId, createData, apiKey, queueSignal) + } catch (error) { + if (queueSignal.aborted) await cancelFalRequest(cancelUrl, apiKey, requestId) + throw error + } +} + +async function pollFalQueue( + endpoint: string, + requestId: string, + createData: Record, + apiKey: string, + signal: AbortSignal +): Promise { const statusUrl = getStringProp(createData, 'status_url') || falQueueUrl(endpoint, requestId, 'status') const responseUrl = getStringProp(createData, 'response_url') || falQueueUrl(endpoint, requestId, 'response') - const maxAttempts = Math.ceil(getMaxExecutionTimeout() / POLL_INTERVAL_MS) - for (let attempt = 0; attempt < maxAttempts; attempt++) { - await sleep(POLL_INTERVAL_MS) + for (;;) { + await interruptibleSleep(POLL_INTERVAL_MS, signal) + signal.throwIfAborted() - const statusResponse = await fetch(statusUrl, { headers: { Authorization: `Key ${apiKey}` } }) + const statusResponse = await fetch(statusUrl, { + headers: { Authorization: `Key ${apiKey}` }, + signal, + }) if (!statusResponse.ok) { const body = await readResponseTextWithLimit(statusResponse, { maxBytes: DEFAULT_MAX_ERROR_BODY_BYTES, @@ -140,6 +204,7 @@ export async function runFalQueue( } const resultResponse = await fetch(getStringProp(statusData, 'response_url') || responseUrl, { headers: { Authorization: `Key ${apiKey}` }, + signal, }) if (!resultResponse.ok) { const body = await readResponseTextWithLimit(resultResponse, { @@ -162,8 +227,49 @@ export async function runFalQueue( throw new Error(`Fal.ai generation failed: ${falErrorMessage(statusData.error)}`) } } +} + +/** + * Asks fal to drop a request nobody will read: a queued request never runs, and a running + * one is signalled to stop. Best effort; the generation already failed either way. + */ +async function cancelFalRequest(cancelUrl: string, apiKey: string, requestId: string) { + try { + // boundary-raw-fetch: cancels a request on the external Fal.ai queue + const response = await fetch(cancelUrl, { + method: 'PUT', + headers: { Authorization: `Key ${apiKey}` }, + signal: AbortSignal.timeout(FAL_CANCEL_TIMEOUT_MS), + }) + await response.body?.cancel() + // 400 means the request already finished, so there is nothing left to stop. + if (!response.ok && response.status !== 400) + throw new Error(`Fal.ai cancellation returned ${response.status}`) + } catch (error) { + logger.warn('Fal.ai request cancellation failed', { + requestId, + error: getErrorMessage(error), + }) + } +} - throw new Error('Fal.ai generation timed out') +/** Records a completed generation's cost; called at most once per generation. */ +export type FalCostRecorder = (cost: FalAICostMetadata) => Promise + +/** + * Bills a request fal has completed. fal charges for it whatever happens next, so the lookup + * ignores the caller's Stop (it has its own bound) and runs before anything that can still fail + * the generation on our side: reading the media URL and the download a Stop can end. + */ +export async function recordFalCost( + apiKey: string, + endpointId: string, + requestId: string, + onCost: FalCostRecorder +): Promise { + const cost = await getFalAICostMetadata({ apiKey, endpointId, requestId }) + await onCost(cost) + return cost } /** @@ -192,8 +298,10 @@ export function extractFalMediaUrl( /** Securely download a generated media URL (or inline data URI) to a buffer. */ export async function downloadFalMedia( - url: string + url: string, + signal?: AbortSignal ): Promise<{ buffer: Buffer; contentType: string }> { + signal?.throwIfAborted() if (url.startsWith('data:')) { const match = /^data:([^;]+);base64,(.+)$/u.exec(url) if (!match) throw new Error('Invalid data URI media response') @@ -202,7 +310,7 @@ export async function downloadFalMedia( return { contentType: match[1], buffer } } - const validation = await validateUrlWithDNS(url, 'mediaUrl', 'contentFetch') + const validation = await validateUrlWithDNS(url, 'mediaUrl', 'contentFetch', { signal }) if (!validation.isValid) { throw new Error(validation.error) } @@ -211,6 +319,7 @@ export async function downloadFalMedia( profile: 'contentFetch', method: 'GET', maxResponseBytes: MAX_MEDIA_BYTES, + signal, }) if (!response.ok) { await readResponseTextWithLimit(response, { diff --git a/apps/sim/lib/mothership/__integration__/content-delivery.integration.ts b/apps/sim/lib/mothership/__integration__/content-delivery.integration.ts index 70c74fca5a3..98997432a34 100644 --- a/apps/sim/lib/mothership/__integration__/content-delivery.integration.ts +++ b/apps/sim/lib/mothership/__integration__/content-delivery.integration.ts @@ -141,6 +141,8 @@ import { generateImageServerTool } from '@/lib/mothership/tools/server/image/gen import { generateAudioServerTool } from '@/lib/mothership/tools/server/media/generate-audio' import { generateVideoServerTool } from '@/lib/mothership/tools/server/media/generate-video' import { getShareForResource } from '@/lib/public-shares/manager' +import { TABLE_DELEGATION_AUDIENCE } from '@/lib/table/application/authorization' +import { queryTableRows } from '@/lib/table/application/rows' import { createOwnedWorkspaceFile, getWorkspaceFile, @@ -582,6 +584,45 @@ describe('content reaching Chat is admitted per item', () => { expectPerItem(read, { secretAdmitted: true }) }) + it('exports private evidence only for rows delivered by the mixed Chat page', async () => { + const scope = { userId: fixture.ids.aliceId, workspaceId: ws() } + const registry = new ResolvedSecretTraceRegistry([], scope) + const result = await observeContentDelivery( + createRegistryContentGate({ registry, actorUserId: scope.userId, workspaceId: ws() }), + () => + queryTableRows.execute({ + principal: createCopilotChatPrincipal(scope, TABLE_DELEGATION_AUDIENCE, { + tableId: fixture.table.tableId, + }), + input: { tableId: fixture.table.tableId, includePersistedSecretProvenance: true }, + }) + ) + expect(result.rows.map((row) => row.id).sort()).toEqual( + [fixture.table.rows.clean, fixture.table.rows.entries].sort() + ) + expect(result.secretProvenance?.complete).toBe(true) + const receiver = new ResolvedSecretTraceRegistry([], scope) + expect( + await receiver.importProvenance(result.secretProvenance, { + trusted: true, + origin: 'delivered-table-page', + }) + ).toBe(true) + const projected = projectResolvedSecretModelContent( + result.rows.map((row) => row.data), + receiver + ) + expect(projected.safe).toBe(true) + expect(JSON.stringify(projected)).not.toContain(CANARY.secret) + expect(JSON.stringify(projected)).toContain('{{FIXTURE_TOKEN}}') + const ordinary = await queryTableRows.execute({ + principal: createSessionPrincipal({ userId: scope.userId }), + input: { tableId: fixture.table.tableId, includePersistedSecretProvenance: true }, + }) + expect(ordinary.rows).toHaveLength(3) + expect(ordinary.secretProvenance?.complete).toBe(false) + }) + it('filters and sorts mixed-provenance rows without refusing unrelated content (#18b)', async () => { const table = `/api/v2/tables/${fixture.table.tableId}` const predicate = { field: 'note', op: 'contains', value: 'second' } diff --git a/apps/sim/lib/mothership/agent-cli/file-provenance.test.ts b/apps/sim/lib/mothership/agent-cli/file-provenance.test.ts index a0638b0f092..96a707dbcfa 100644 --- a/apps/sim/lib/mothership/agent-cli/file-provenance.test.ts +++ b/apps/sim/lib/mothership/agent-cli/file-provenance.test.ts @@ -374,7 +374,7 @@ describe('file provenance at the actual CLI and model-result boundary', () => { }) it('refuses only the unknown item of a forwarded response, naming it, without blanking the invocation', async () => { - const refusals = getMockLogger('DurableSecretProvenancePersistence').error + const refusals = getMockLogger('DurableSecretProvenancePersistence').warn refusals.mockClear() const clean = fileSource('clean', { status: 'exact', entries: [] }) const tainted = fileSource('tainted', { status: 'unknown' }) @@ -552,7 +552,7 @@ describe('file provenance at the actual CLI and model-result boundary', () => { }) ) classifySequence('exact', 'unknown') - const refusals = getMockLogger('DurableSecretProvenancePersistence').error + const refusals = getMockLogger('DurableSecretProvenancePersistence').warn refusals.mockClear() const trace = registry() const result = await runCli(grepFiles('CANARY_FOR_LOCAL_TEST'), trace) @@ -569,6 +569,12 @@ describe('file provenance at the actual CLI and model-result boundary', () => { resourceId: tainted.id, }) ) + /** The model is told the file it cannot see exists, so it does not read the gap as absence. */ + expect(result.output).toMatchObject({ + stderr: expect.stringContaining( + 'Note: 1 file was withheld from this result: its secret provenance is unknown, so reading it again will not return it.' + ), + }) }) describe('grep reshapes a file line only after redacting it', () => { diff --git a/apps/sim/lib/mothership/agent-cli/index.ts b/apps/sim/lib/mothership/agent-cli/index.ts index 4d7a7b525dd..bfc76115ffb 100644 --- a/apps/sim/lib/mothership/agent-cli/index.ts +++ b/apps/sim/lib/mothership/agent-cli/index.ts @@ -1,4 +1,5 @@ import { createEmbeddedClient, type EmbeddedCliIdentity } from 'sim/embed' +import { countWithheldContent } from '@/lib/core/application' import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { curateBlockDetail } from '@/lib/mothership/agent-cli/curation' @@ -16,6 +17,7 @@ import { createCopilotChatPrincipal, createTrustedOrganizationCopilotPrincipal, } from '@/lib/mothership/auth/application-delegation' +import { describeWithheldContent } from '@/lib/mothership/content-delivery/telemetry' import type { AgentCliRawResult, AgentCliRequest } from '@/lib/mothership/generated/agent-cli' import type { ResourceChange } from '@/lib/mothership/generated/resources' import { TraceSpan } from '@/lib/mothership/generated/trace-spans-v1' @@ -38,6 +40,18 @@ export interface AgentCliExecutionContext extends Omit { + const { result, withheld } = await countWithheldContent(() => + executeUncountedAgentCliRequest(request, context) + ) + const note = describeWithheldContent(withheld) + if (!note) return result + return { ...result, stderr: result.stderr ? `${result.stderr}\n${note}` : note } +} + +async function executeUncountedAgentCliRequest( + request: AgentCliRequest, + context: AgentCliExecutionContext ): Promise { context.signal?.throwIfAborted() if (context.readOnly && !isReadOnlyCliRequest(request)) { diff --git a/apps/sim/lib/mothership/async-runs/browser-download-claim.integration.ts b/apps/sim/lib/mothership/async-runs/browser-download-claim.integration.ts index 236672f338d..ec6f34988ba 100644 --- a/apps/sim/lib/mothership/async-runs/browser-download-claim.integration.ts +++ b/apps/sim/lib/mothership/async-runs/browser-download-claim.integration.ts @@ -53,6 +53,7 @@ describe('browser download admission with PostgreSQL', () => { execution_settled_at timestamp, execution_owner_token text, execution_lease_expires_at timestamptz, execution_revoked_at timestamptz, execution_attempts integer NOT NULL DEFAULT 0, + execution_deadline_at timestamptz, client_workflow_execution_id text, sandbox_processes jsonb NOT NULL DEFAULT '{}', completed_at timestamp, created_at timestamp NOT NULL DEFAULT now(), updated_at timestamp NOT NULL DEFAULT now() diff --git a/apps/sim/lib/mothership/async-runs/execution-lease.ts b/apps/sim/lib/mothership/async-runs/execution-lease.ts index 036014c33a7..8e3a3f4bdf3 100644 --- a/apps/sim/lib/mothership/async-runs/execution-lease.ts +++ b/apps/sim/lib/mothership/async-runs/execution-lease.ts @@ -19,6 +19,16 @@ export const SIM_TOOL_EXECUTION_MAX_ATTEMPTS = 3 export const INTERRUPTED_SIM_TOOL_MESSAGE = 'Tool execution lost its owner before a result was recorded. Its outcome is unknown; inspect current state before retrying a mutation.' +/** + * A call starts, the first time or again after a takeover, only with at least this much of its + * deadline left; otherwise it fails as not run instead of starting work that would likely be cut + * off with its outcome unknown. About 99% of non-workflow tool calls finish within a minute. + */ +export const SIM_TOOL_MIN_START_BUDGET_MS = 60_000 + +export const DEADLINE_SIM_TOOL_MESSAGE = + 'Tool execution was interrupted because the Sim server running it stopped, and too little of its time remained to run it again. Its outcome is unknown; inspect current state before retrying a mutation.' + export const EXHAUSTED_SIM_TOOL_MESSAGE = `Tool execution was interrupted ${SIM_TOOL_EXECUTION_MAX_ATTEMPTS} times because the Sim server running it stopped, so it was not run again. Its outcome is unknown; inspect current state before retrying a mutation.` export class SimToolExecutionLeaseLostError extends Error { diff --git a/apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts b/apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts index 653dce5adb4..bb2cb9becff 100644 --- a/apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts +++ b/apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts @@ -262,6 +262,56 @@ describe.runIf(Boolean(redisUrl))('Chat runs no controller owns', () => { return { ...run, marker: chat?.conversationId ?? null } } + describe('a Sim claim past its start window', () => { + async function lateTool( + runId: string, + fields: Partial<{ status: 'running'; claimedBy: string }> = {} + ) { + const tool = await dispatchedTool(runId) + await db + .update(copilotAsyncToolCalls) + .set({ executionDeadlineAt: sql`clock_timestamp() + interval '30 seconds'`, ...fields }) + .where(eq(copilotAsyncToolCalls.toolCallId, tool.toolCallId)) + return tool + } + + async function storedTool(toolCallId: string) { + const [row] = await db + .select() + .from(copilotAsyncToolCalls) + .where(eq(copilotAsyncToolCalls.toolCallId, toolCallId)) + return row + } + + it('leaves an admitted pending call unclaimed for its caller to settle as never run', async () => { + const run = await admittedRun() + const tool = await lateTool(run.runId) + + expect(await claimToolExecution(tool)).toEqual({ outcome: 'too_late' }) + expect(await storedTool(tool.toolCallId)).toMatchObject({ + status: 'pending', + executionStartedAt: null, + executionOwnerToken: null, + }) + }) + + it('reports a stopped run as closed before the start window', async () => { + const run = await admittedRun({ stopped: true }) + const tool = await lateTool(run.runId) + + expect(await claimToolExecution(tool)).toEqual({ outcome: 'closed' }) + }) + + it('still claims a call a client already claimed, and one without a deadline', async () => { + const run = await admittedRun() + const clientClaimed = await lateTool(run.runId, { status: 'running', claimedBy: 'client' }) + const undated = await dispatchedTool(run.runId) + + expect(await claimToolExecution(clientClaimed)).toEqual({ outcome: 'claimed' }) + expect(await claimToolExecution(undated)).toEqual({ outcome: 'claimed' }) + }) + }) + it('settles a run whose controller died once its recovery window has passed', async () => { const orphan = await admittedRun({ idleMinutes: 90, status: 'paused_waiting_for_tool' }) diff --git a/apps/sim/lib/mothership/async-runs/repository.test.ts b/apps/sim/lib/mothership/async-runs/repository.test.ts index 7ae9662d040..dbbae17f179 100644 --- a/apps/sim/lib/mothership/async-runs/repository.test.ts +++ b/apps/sim/lib/mothership/async-runs/repository.test.ts @@ -148,9 +148,10 @@ describe('run admission and early Stop', () => { [copilotRequestStops.workspaceId, scope.workspaceId], [copilotRequestStops.streamId, scope.streamId], ]) { + /** The chat row is read first; the Stop lookup follows it. */ expect( hasMockCondition( - dbChainMockFns.where.mock.calls[0]?.[0], + dbChainMockFns.where.mock.calls[1]?.[0], (condition) => condition.type === 'eq' && condition.left === column && condition.right === value ) diff --git a/apps/sim/lib/mothership/async-runs/repository.ts b/apps/sim/lib/mothership/async-runs/repository.ts index 42a9a122265..d0bee161c12 100644 --- a/apps/sim/lib/mothership/async-runs/repository.ts +++ b/apps/sim/lib/mothership/async-runs/repository.ts @@ -32,14 +32,17 @@ import { } from 'drizzle-orm' import { type ResourceOwner, resourceScopeFromOwner } from '@/lib/core/resource-scope' import { acquireAdvisoryXactLock } from '@/lib/db/advisory-locks' +import type { DbOrTx, DbTransaction } from '@/lib/db/types' import type { SessionProcessIdentity } from '@/lib/execution/remote-sandbox/session-process' import { AsyncToolCallOwnershipError } from '@/lib/mothership/async-runs/errors' import { executableToolPermission } from '@/lib/mothership/async-runs/executable-tool-permission' import { + DEADLINE_SIM_TOOL_MESSAGE, EXHAUSTED_SIM_TOOL_MESSAGE, INTERRUPTED_SIM_TOOL_MESSAGE, SIM_TOOL_EXECUTION_LEASE_SECONDS, SIM_TOOL_EXECUTION_MAX_ATTEMPTS, + SIM_TOOL_MIN_START_BUDGET_MS, SimToolExecutionLeaseLostError, type SimToolExecutionOwner, } from '@/lib/mothership/async-runs/execution-lease' @@ -52,6 +55,7 @@ import { isAwaitingToolPermission, SIM_TOOL_EXECUTION_VERSION, } from '@/lib/mothership/async-runs/lifecycle' +import { TOOL_WATCHDOG_RESUME_GRACE_MS } from '@/lib/mothership/constants' import { TraceAttr } from '@/lib/mothership/generated/trace-attributes-v1' import { TraceSpan } from '@/lib/mothership/generated/trace-spans-v1' import { @@ -194,34 +198,159 @@ export async function requestRunStop( (input.chatId && run.chatId !== input.chatId)) ) return run - const { userId, streamId } = input - const owner = resourceScopeFromOwner(input) - if (owner.kind === 'organization') { - await tx - .insert(copilotOrganizationRequestStops) - .values({ userId, organizationId: owner.organizationId, streamId }) - .onConflictDoNothing() - } else { - await tx - .insert(copilotRequestStops) - .values({ userId, workspaceId: owner.workspaceId, streamId }) - .onConflictDoNothing() - } - if (run) { - const stoppedRuns = and( - eq(copilotRuns.userId, input.userId), - eq(copilotRuns.streamId, input.streamId) - ) - await tx - .update(copilotRuns) - .set({ toolAdmissionClosedAt: sql`coalesce(${copilotRuns.toolAdmissionClosedAt}, now())` }) - .where(stoppedRuns) - await cancelOpenDesktopToolCalls(tx, stoppedRuns, input.userId) - } + await recordRunStopTx(tx, input, Boolean(run)) return run ?? null }) } +/** + * Records Stop for one stream in the caller's transaction: the durable intent every later + * segment and the worker's run-control read observe, closed tool admission, and settled + * desktop calls. `admitted` is false when no run row exists yet to close. + */ +async function recordRunStopTx(tx: DbTransaction, input: RunStopInput, admitted = true) { + const { userId, streamId } = input + const owner = resourceScopeFromOwner(input) + if (owner.kind === 'organization') { + await tx + .insert(copilotOrganizationRequestStops) + .values({ userId, organizationId: owner.organizationId, streamId }) + .onConflictDoNothing() + } else { + await tx + .insert(copilotRequestStops) + .values({ userId, workspaceId: owner.workspaceId, streamId }) + .onConflictDoNothing() + } + if (!admitted) return + const stoppedRuns = and(eq(copilotRuns.userId, userId), eq(copilotRuns.streamId, streamId)) + await tx + .update(copilotRuns) + .set({ toolAdmissionClosedAt: sql`coalesce(${copilotRuns.toolAdmissionClosedAt}, now())` }) + .where(stoppedRuns) + await cancelOpenDesktopToolCalls(tx, stoppedRuns, userId) +} + +type StoppedSandboxProcess = SessionProcessIdentity & { toolCallId: string } + +/** The run a Stop is delivered to. */ +export interface RunStopTarget { + id: string + streamId: string + userId: string + chatId: string + desktopDeviceId: string | null +} + +/** The client workflows and sandbox commands a stopped run started, read once its admission closed. */ +export interface StoppedRunWork { + clientWorkflowExecutionIds: string[] + sandboxProcesses: StoppedSandboxProcess[] +} + +/** + * A run a removal stopped. Its work is read in the removal's own transaction, before a hard + * delete cascades those rows away. + */ +export interface StoppedRun extends RunStopTarget, StoppedRunWork {} + +type LiveRunScope = + | { chatIds: string[] } + | { userId: string } + | { workspaceId: string } + | { userId: string; workspaceIds: string[] } + | { userId: string; organizationId: string } + +function liveRunScopeCondition(scope: LiveRunScope): SQL | undefined { + if ('chatIds' in scope) return inArray(copilotRuns.chatId, scope.chatIds) + if ('organizationId' in scope) + return and( + eq(copilotRuns.userId, scope.userId), + eq(copilotRuns.organizationId, scope.organizationId) + ) + if ('workspaceIds' in scope) + return and( + eq(copilotRuns.userId, scope.userId), + inArray(copilotRuns.workspaceId, scope.workspaceIds) + ) + if ('workspaceId' in scope) return eq(copilotRuns.workspaceId, scope.workspaceId) + return eq(copilotRuns.userId, scope.userId) +} + +/** Of these workspaces, the ones where the user still has an unfinished Chat run. */ +export async function findWorkspacesWithLiveRunsTx( + tx: DbTransaction, + userId: string, + workspaceIds: string[] +): Promise { + if (workspaceIds.length === 0) return [] + const rows = await tx + .selectDistinct({ workspaceId: copilotRuns.workspaceId }) + .from(copilotRuns) + .where( + and( + eq(copilotRuns.userId, userId), + inArray(copilotRuns.workspaceId, workspaceIds), + notInArray(copilotRuns.status, TERMINAL_RUN_STATUSES) + ) + ) + return rows.flatMap(({ workspaceId }) => (workspaceId ? [workspaceId] : [])) +} + +/** + * Stops every unfinished run in `scope` inside the removal's own transaction, so Stop commits + * exactly when the removal does: a restored chat cannot revive the run and a takeover never + * adopts it. Rows a hard delete cascades away take their Stop with them; run control then + * answers Stop because the chat or owner is gone. Callers deliver the returned runs' Stop with + * `deliverRunStops` after commit. + */ +export async function stopLiveRunsTx( + tx: DbTransaction, + scope: LiveRunScope +): Promise { + if ( + ('chatIds' in scope && scope.chatIds.length === 0) || + ('workspaceIds' in scope && scope.workspaceIds.length === 0) + ) + return [] + const rows = await tx + .select({ + id: copilotRuns.id, + streamId: copilotRuns.streamId, + userId: copilotRuns.userId, + chatId: copilotRuns.chatId, + workspaceId: copilotRuns.workspaceId, + organizationId: copilotRuns.organizationId, + desktopDeviceId: copilotRuns.desktopDeviceId, + }) + .from(copilotRuns) + .where(and(liveRunScopeCondition(scope), notInArray(copilotRuns.status, TERMINAL_RUN_STATUSES))) + const stopped: StoppedRun[] = [] + for (const { + id, + streamId, + userId, + chatId, + workspaceId, + organizationId, + desktopDeviceId, + } of rows) { + /** A run with no single owner has no Stop row to write; run control already answers Stop. */ + if (Boolean(workspaceId) !== Boolean(organizationId)) + await recordRunStopTx(tx, { streamId, userId, workspaceId, organizationId }) + stopped.push({ + id, + streamId, + userId, + chatId, + desktopDeviceId, + clientWorkflowExecutionIds: await getUnsettledClientWorkflowExecutions(streamId, userId, tx), + sandboxProcesses: await getUnsettledStreamSandboxProcesses(streamId, userId, tx), + }) + } + return stopped +} + /** * Settles the stopped runs' open desktop calls in the transaction that closes their admission, so * none can be claimed afterwards and none is left looking live. A call nobody claimed never @@ -313,28 +442,31 @@ export async function createRunSegment(input: CreateRunSegmentInput) { ) } -/** Caller holds the Stop/admission lock; its related chat writes commit with this run. */ +/** + * Caller holds the Stop/admission lock; its related chat writes commit with this run. The + * chat row is read FOR SHARE, after that lock as chat admission takes it, so a soft delete + * either commits first and this run starts stopped, or waits and then stops this run. + */ export async function insertRunSegment(tx: RunAdmissionTransaction, input: CreateRunSegmentInput) { - let workspaceId = input.workspaceId - let organizationId = input.organizationId - if (!workspaceId && !organizationId) { - const [chat] = await tx - .select({ - workspaceId: copilotChats.workspaceId, - organizationId: copilotChats.organizationId, - }) - .from(copilotChats) - .where(and(eq(copilotChats.id, input.chatId), eq(copilotChats.userId, input.userId))) - .limit(1) - workspaceId = chat?.workspaceId - organizationId = chat?.organizationId - } + const [chat] = await tx + .select({ + workspaceId: copilotChats.workspaceId, + organizationId: copilotChats.organizationId, + deletedAt: copilotChats.deletedAt, + }) + .from(copilotChats) + .where(and(eq(copilotChats.id, input.chatId), eq(copilotChats.userId, input.userId))) + .for('share') + const inheritsOwner = !input.workspaceId && !input.organizationId + const workspaceId = inheritsOwner ? chat?.workspaceId : input.workspaceId + const organizationId = inheritsOwner ? chat?.organizationId : input.organizationId const { table, predicate } = stopLocation({ ...input, workspaceId, organizationId }) const [stop] = await tx .select({ stoppedAt: table.stoppedAt }) .from(table) .where(predicate) .limit(1) + const closedAt = stop?.stoppedAt ?? chat?.deletedAt const [run] = await tx .insert(copilotRuns) .values({ @@ -353,8 +485,8 @@ export async function insertRunSegment(tx: RunAdmissionTransaction, input: Creat provider: input.provider ?? null, requestContext: input.requestContext ?? {}, desktopDeviceId: input.desktopDeviceId ?? null, - status: stop ? 'cancelled' : (input.status ?? 'active'), - ...(stop ? { completedAt: sql`now()`, toolAdmissionClosedAt: stop.stoppedAt } : {}), + status: closedAt ? 'cancelled' : (input.status ?? 'active'), + ...(closedAt ? { completedAt: sql`now()`, toolAdmissionClosedAt: closedAt } : {}), }) .returning() if (!run) throw new Error('Run admission did not return its persisted identity') @@ -497,6 +629,8 @@ export async function upsertAsyncToolCall(input: { sealedContext?: AsyncCompletionData /** The call is held for the user's decision before anything may run it. */ permissionRequested?: boolean + /** When the call must be settled by; the first row written for the call keeps its own. */ + executionDeadlineAt?: Date }) { return await withDbSpan( TraceSpan.CopilotAsyncRunsUpsertAsyncToolCall, @@ -543,6 +677,9 @@ export async function upsertAsyncToolCall(input: { status: incomingStatus, ...(sealedContext !== undefined ? { result: sealedContext } : {}), ...(input.permissionRequested ? { permissionRequestedAt: now } : {}), + ...(input.executionDeadlineAt + ? { executionDeadlineAt: input.executionDeadlineAt } + : {}), updatedAt: now, }) .onConflictDoNothing() @@ -734,6 +871,12 @@ export type ToolExecutionClaim = /** Already claimed or settled. */ | { outcome: 'existing' } +/** A Sim handler's claim, which also reports an unclaimed call too late to start. */ +export type SimToolExecutionClaim = + | ToolExecutionClaim + /** Admitted but unclaimed, with too little of its deadline left to start: nothing ran it. */ + | { outcome: 'too_late' } + /** * Every condition at once, typed as a condition: drizzle's `and` may return undefined, which a * negation would render as `NOT (NULL)` and match nothing. @@ -758,15 +901,35 @@ const unfinishedSimExecution = allOf( isNull(copilotAsyncToolCalls.clientWorkflowExecutionId) ) +/** + * A call whose deadline leaves less than the minimum start budget, so nothing may start it: no + * re-run and no claim. A call without a deadline never matches. + */ +const tooLateToStart = sql`(${copilotAsyncToolCalls.executionDeadlineAt} IS NOT NULL AND ${copilotAsyncToolCalls.executionDeadlineAt} < clock_timestamp() + ${SIM_TOOL_MIN_START_BUDGET_MS} * interval '1 millisecond')` + +/** + * A call past its deadline plus the resume grace: the resume wait's ceiling for it (`run.ts`), past + * which nothing still waits for its result. A call without a deadline never matches. + */ +const pastResumeGiveUp = sql`(${copilotAsyncToolCalls.executionDeadlineAt} IS NOT NULL AND ${copilotAsyncToolCalls.executionDeadlineAt} + ${TOOL_WATCHDOG_RESUME_GRACE_MS} * interval '1 millisecond' <= clock_timestamp())` + +/** A call nothing claimed or started whose deadline leaves too little to start it. */ +const pendingTooLateToStart = allOf( + eq(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.pending), + isNull(copilotAsyncToolCalls.executionStartedAt), + tooLateToStart +) + /** * A Sim execution whose owner died before recording a result: its lease lapsed, which a live * owner's heartbeat never lets happen. A takeover runs it again until it has started - * {@link SIM_TOOL_EXECUTION_MAX_ATTEMPTS} times. + * {@link SIM_TOOL_EXECUTION_MAX_ATTEMPTS} times, while enough of its deadline remains. */ const interruptedSimExecution = allOf( unfinishedSimExecution, sql`${copilotAsyncToolCalls.executionLeaseExpiresAt} <= clock_timestamp()`, - lt(copilotAsyncToolCalls.executionAttempts, SIM_TOOL_EXECUTION_MAX_ATTEMPTS) + lt(copilotAsyncToolCalls.executionAttempts, SIM_TOOL_EXECUTION_MAX_ATTEMPTS), + not(tooLateToStart) ) /** @@ -785,11 +948,12 @@ const rerunnableSimExecution = allOf( * Sim claims a call it is about to execute under an execution lease; a terminal result never * releases it. The claim takes a call's first execution, or re-runs one whose owner died * mid-execution: the new owner token fences the dead owner out, since its renewals and its - * result both require the token and an unexpired lease. + * result both require the token and an unexpired lease. A pending call too late to start is left + * unclaimed, under the same admission check, for its caller to settle as never run. */ export async function claimToolExecution( owner: SimToolExecutionOwner -): Promise { +): Promise { return await claimUnderRunAdmission(owner, 'sim-stream', true, async (tx, thisCall) => { const startedAt = new Date() const [claimed] = await traceMothershipQuery('UPDATE', 'copilot_async_tool_calls', () => @@ -814,7 +978,8 @@ export async function claimToolExecution( inArray(copilotAsyncToolCalls.status, [ ASYNC_TOOL_STATUS.pending, ASYNC_TOOL_STATUS.running, - ]) + ]), + not(pendingTooLateToStart) ), interruptedSimExecution ) @@ -824,10 +989,13 @@ export async function claimToolExecution( ) if (claimed) return { outcome: 'claimed' } as const const [record] = await traceMothershipQuery('SELECT', 'copilot_async_tool_calls', () => - tx.select({ id: copilotAsyncToolCalls.id }).from(copilotAsyncToolCalls).where(thisCall) + tx + .select({ tooLate: sql`${pendingTooLateToStart}` }) + .from(copilotAsyncToolCalls) + .where(thisCall) ) if (!record) throw new Error('Tool execution record is unavailable') - return { outcome: 'existing' } as const + return record.tooLate ? ({ outcome: 'too_late' } as const) : ({ outcome: 'existing' } as const) }) } @@ -893,6 +1061,7 @@ export async function claimDesktopToolCall( and( thisCall, eq(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.pending), + not(tooLateToStart), // The executor takes only a call Sim offered it, inside its pickup window; once the // window closed the call belongs to its not-started settlement. executor @@ -928,7 +1097,8 @@ export async function claimDesktopToolCall( /** * Expired ownership cannot be renewed, even before a follower has observed the expiry. With * `desktop`, the call must still be running on a run bound to that device, so a call Stop settled - * cannot be renewed either. + * cannot be renewed either, nor one past its deadline plus the resume grace: its lease then lapses + * and the call settles as outcome unknown. */ export async function renewSimToolExecutionLease( owner: SimToolExecutionOwner, @@ -951,6 +1121,7 @@ export async function renewSimToolExecutionLease( desktop ? and( eq(copilotAsyncToolCalls.status, ASYNC_TOOL_STATUS.running), + not(pastResumeGiveUp), 'deviceId' in desktop ? sql`EXISTS (SELECT 1 FROM ${copilotRuns} r WHERE r.id = ${copilotAsyncToolCalls.runId} AND r.desktop_device_id = ${desktop.deviceId})` : and( @@ -994,8 +1165,11 @@ export async function getChatViewDesktopLeaseRemainingMs( return row ? Number(row.remainingMs) : null } -/** Why a lapsed execution ended without a result: once, or after every re-run it was allowed. */ -const interruptionMessage = sql`CASE WHEN ${copilotAsyncToolCalls.executionAttempts} >= ${SIM_TOOL_EXECUTION_MAX_ATTEMPTS} THEN ${EXHAUSTED_SIM_TOOL_MESSAGE}::text ELSE ${INTERRUPTED_SIM_TOOL_MESSAGE}::text END` +/** + * Why a lapsed execution ended without a result: once, after every re-run it was allowed, or + * with too little of its deadline left to run again. + */ +const interruptionMessage = sql`CASE WHEN ${copilotAsyncToolCalls.executionAttempts} >= ${SIM_TOOL_EXECUTION_MAX_ATTEMPTS} THEN ${EXHAUSTED_SIM_TOOL_MESSAGE}::text WHEN ${tooLateToStart} THEN ${DEADLINE_SIM_TOOL_MESSAGE}::text ELSE ${INTERRUPTED_SIM_TOOL_MESSAGE}::text END` /** * Whether one of the run's Sim handlers is still running with its execution lease held. Its @@ -1040,8 +1214,9 @@ export async function revokeExpiredSimToolExecutions(input: { } /** - * Fails the run's lapsed executions that already started every time they may, so a takeover - * hands the model a clear failure for each instead of running it again. + * Fails the run's lapsed executions that already started every time they may, or whose + * deadline leaves too little to run again, so a takeover hands the model a clear failure for + * each instead of running it again. */ export async function revokeExhaustedSimToolExecutions(runId: string) { return db.transaction((tx) => @@ -1049,7 +1224,10 @@ export async function revokeExhaustedSimToolExecutions(runId: string) { tx, and( eq(copilotAsyncToolCalls.runId, runId), - gte(copilotAsyncToolCalls.executionAttempts, SIM_TOOL_EXECUTION_MAX_ATTEMPTS) + or( + gte(copilotAsyncToolCalls.executionAttempts, SIM_TOOL_EXECUTION_MAX_ATTEMPTS), + tooLateToStart + ) ) ) ) @@ -1235,15 +1413,16 @@ export async function settleSimSandboxProcess( export async function getUnsettledStreamSandboxProcesses( streamId: string, - userId: string -): Promise> { + userId: string, + executor: DbOrTx = db +): Promise { return await withDbSpan( TraceSpan.CopilotAsyncRunsGetRunSegment, 'SELECT', 'copilot_async_tool_calls', { [TraceAttr.StreamId]: streamId, [TraceAttr.UserId]: userId }, async () => { - const rows = await db + const rows = await executor .select({ toolCallId: copilotAsyncToolCalls.toolCallId, processes: copilotAsyncToolCalls.sandboxProcesses, @@ -1545,14 +1724,18 @@ export async function areStreamToolExecutionsSettled( } /** Execution IDs reserved by this run's browser-facing workflow handlers, independent of tool results. */ -export async function getUnsettledClientWorkflowExecutions(streamId: string, userId: string) { +export async function getUnsettledClientWorkflowExecutions( + streamId: string, + userId: string, + executor: DbOrTx = db +) { return await withDbSpan( TraceSpan.CopilotAsyncRunsGetRunSegment, 'SELECT', 'copilot_async_tool_calls', { [TraceAttr.StreamId]: streamId, [TraceAttr.UserId]: userId }, async () => { - const rows = await db + const rows = await executor .select({ executionId: copilotAsyncToolCalls.clientWorkflowExecutionId }) .from(copilotAsyncToolCalls) .innerJoin(copilotRuns, eq(copilotRuns.id, copilotAsyncToolCalls.runId)) @@ -1627,6 +1810,7 @@ export async function claimWorkflowToolExecution( .where( and( eq(copilotAsyncToolCalls.toolCallId, toolCallId), + not(tooLateToStart), or( and( isNull(copilotAsyncToolCalls.claimedBy), diff --git a/apps/sim/lib/mothership/chat/application/context.ts b/apps/sim/lib/mothership/chat/application/context.ts index c12c348830f..56aa4e2b04d 100644 --- a/apps/sim/lib/mothership/chat/application/context.ts +++ b/apps/sim/lib/mothership/chat/application/context.ts @@ -6,6 +6,7 @@ import { getActivelyBannedUserIds } from '@/lib/auth/ban' import type { WorkspaceAuthorizationContext } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' import { conversationModeSelection } from '@/lib/mothership/chat/intent' +import { authorizeChatWorkflowRead, isChatModeAvailable } from '@/lib/mothership/chat/lifecycle' import { resolveActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/context' export type ChatOwnerContext = @@ -19,12 +20,25 @@ export async function resolveOwnedWorkspaceChatContext(principal: Principal, cha return context } -export async function resolveOwnedChatContext(principal: Principal, chatId: string) { +export interface OwnedChatContextOptions { + /** + * Resolves the chat for an owner whose account is blocked. Only Stop sets it: halting the + * owner's existing work is never harmful, so it stays available after access is withheld. + */ + allowBlockedOwner?: boolean +} + +export async function resolveOwnedChatContext( + principal: Principal, + chatId: string, + options: OwnedChatContextOptions = {} +) { const [chat] = await db .select({ userId: copilotChats.userId, workspaceId: copilotChats.workspaceId, organizationId: copilotChats.organizationId, + workflowId: copilotChats.workflowId, type: copilotChats.type, mode: conversationModeSelection, memorySpaceId: copilotChats.memorySpaceId, @@ -34,27 +48,62 @@ export async function resolveOwnedChatContext(principal: Principal, chatId: stri .limit(1) if ( !chat || - Boolean(chat.workspaceId) === Boolean(chat.organizationId) || + (chat.workspaceId && chat.organizationId) || + (!chat.workspaceId && !chat.organizationId && !chat.workflowId) || chat.userId !== resolvePrincipalSubjectUserId(principal) || (chat.organizationId && chat.type !== 'mothership') ) { throw new OrchestrationError('not_found', 'Chat not found') } - if ((await getActivelyBannedUserIds([chat.userId])).length > 0) { + if (!options.allowBlockedOwner && (await getActivelyBannedUserIds([chat.userId])).length > 0) { throw new OrchestrationError('forbidden', 'User account is suspended') } const owner: ChatOwnerContext = chat.organizationId ? { organizationId: chat.organizationId } : { - ...(await resolveActiveWorkspaceApplicationContext(chat.workspaceId!)), + ...(await resolveActiveWorkspaceApplicationContext( + await resolveChatWorkspaceId(chatId, chat) + )), organizationId: undefined, } return { ...owner, chatId, userId: chat.userId, + workflowId: chat.workflowId, type: chat.type, mode: chat.mode, memorySpaceId: chat.memorySpaceId ?? null, } } + +/** + * A workflow chat row from before chats carried a workspace is scoped by its workflow's, while + * its owner may still read that workflow. + */ +async function resolveChatWorkspaceId( + chatId: string, + chat: { userId: string; workspaceId: string | null; workflowId: string | null } +): Promise { + if (chat.workspaceId) return chat.workspaceId + const workflow = chat.workflowId + ? await authorizeChatWorkflowRead(chatId, chat.userId, chat.workflowId) + : null + if (!workflow?.workspaceId) throw new OrchestrationError('not_found', 'Chat not found') + return workflow.workspaceId +} + +/** + * Refuses a chat its owner may no longer read, as the chat detail read does: a workflow chat + * whose workflow they cannot read, or a workspace Plan chat while Plan mode is off for them. + */ +export async function requireOwnedChatReadable( + context: Awaited> +) { + if ( + (context.workflowId && + !(await authorizeChatWorkflowRead(context.chatId, context.userId, context.workflowId))) || + !(await isChatModeAvailable(context, context.userId)) + ) + throw new OrchestrationError('not_found', 'Chat not found') +} diff --git a/apps/sim/lib/mothership/chat/application/read-older-messages.ts b/apps/sim/lib/mothership/chat/application/read-older-messages.ts index a6fed1f17f3..48fa778bef3 100644 --- a/apps/sim/lib/mothership/chat/application/read-older-messages.ts +++ b/apps/sim/lib/mothership/chat/application/read-older-messages.ts @@ -3,7 +3,10 @@ import { defineWorkspaceOperation } from '@/lib/core/application' import { defineOrganizationOperation } from '@/lib/core/application/organization-operation' import { OrchestrationError } from '@/lib/core/orchestration/types' import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case' -import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context' +import { + requireOwnedChatReadable, + resolveOwnedChatContext, +} from '@/lib/mothership/chat/application/context' import { loadCopilotChatMessagesBefore } from '@/lib/mothership/chat/lifecycle' import { compactToolCallParams, normalizeMessage } from '@/lib/mothership/chat/persisted-message' import { decodeTranscriptCursor } from '@/lib/mothership/chat/transcript-cursor' @@ -14,7 +17,7 @@ interface ReadChatOlderMessagesInput { } /** - * A page of the owner's mothership chat transcript before a cursor the chat detail read + * A page of the owner's chat transcript before a cursor the chat detail read * issued, normalized and compacted exactly as that read returns its messages, so a message * the backfill has not reached still arrives without its bulk tool-call params. */ @@ -47,7 +50,7 @@ export const readChatOlderMessages = defineAuthorizedChatUseCase({ async execute({ context, input }) { const position = decodeTranscriptCursor(input.before) if (!position) throw new OrchestrationError('validation', 'Invalid cursor') - if (context.type !== 'mothership') throw new OrchestrationError('not_found', 'Chat not found') + await requireOwnedChatReadable(context) const page = await loadCopilotChatMessagesBefore(context.chatId, position) return { messages: page.messages.map((message) => diff --git a/apps/sim/lib/mothership/chat/application/read-stream-status.integration.ts b/apps/sim/lib/mothership/chat/application/read-stream-status.integration.ts index a87dcb62e31..8c63fecf9cd 100644 --- a/apps/sim/lib/mothership/chat/application/read-stream-status.integration.ts +++ b/apps/sim/lib/mothership/chat/application/read-stream-status.integration.ts @@ -1,6 +1,6 @@ /** Exercises the transcript-free stream status read against real PostgreSQL rows. */ import { db } from '@sim/db' -import { copilotChats, permissions, user, workspace } from '@sim/db/schema' +import { copilotChats, permissions, user, workflow, workspace } from '@sim/db/schema' import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' import { generateId } from '@sim/utils/id' @@ -14,10 +14,41 @@ const workspaceId = generateId() const owner = createSessionPrincipal({ userId: ownerId, sessionId: generateId() }) const member = createSessionPrincipal({ userId: memberId, sessionId: generateId() }) -async function createChat(type: 'mothership' | 'copilot'): Promise { +async function createChat(): Promise { const [chat] = await db .insert(copilotChats) - .values({ userId: ownerId, workspaceId, type }) + .values({ userId: ownerId, workspaceId, type: 'mothership' }) + .returning({ id: copilotChats.id }) + return chat.id +} + +/** + * A workflow Copilot panel chat, as the panel stores it. A legacy row, from before chats carried + * a workspace, has only its workflow's. + */ +async function createWorkflowChat( + options: { archivedWorkflow?: boolean; legacyRow?: boolean } = {} +): Promise { + const workflowId = generateId() + const now = new Date() + await db.insert(workflow).values({ + id: workflowId, + userId: ownerId, + workspaceId, + name: `Chat stream status workflow ${workflowId}`, + lastSynced: now, + createdAt: now, + updatedAt: now, + archivedAt: options.archivedWorkflow ? now : null, + }) + const [chat] = await db + .insert(copilotChats) + .values({ + userId: ownerId, + workflowId, + workspaceId: options.legacyRow ? null : workspaceId, + type: 'copilot', + }) .returning({ id: copilotChats.id }) return chat.id } @@ -60,21 +91,31 @@ afterAll(async () => { describe('reading whether a chat is still streaming', () => { it('answers the owner of an idle chat with no active stream', async () => { - const chatId = await createChat('mothership') + const chatId = await createChat() await expect( readChatStreamStatus.execute({ principal: owner, input: { chatId } }) ).resolves.toEqual({ activeStreamId: null }) }) it('refuses another member of the workspace, as the chat detail read does', async () => { - const chatId = await createChat('mothership') + const chatId = await createChat() await expect( readChatStreamStatus.execute({ principal: member, input: { chatId } }) ).rejects.toThrow('Chat not found') }) - it('refuses a workflow copilot chat, which the chat detail read does not serve', async () => { - const chatId = await createChat('copilot') + it('answers the owner of a workflow copilot chat, including a legacy row without a workspace', async () => { + for (const chatId of [ + await createWorkflowChat(), + await createWorkflowChat({ legacyRow: true }), + ]) + await expect( + readChatStreamStatus.execute({ principal: owner, input: { chatId } }) + ).resolves.toEqual({ activeStreamId: null }) + }) + + it('refuses a workflow copilot chat whose workflow is archived, as the chat detail read does', async () => { + const chatId = await createWorkflowChat({ archivedWorkflow: true }) await expect( readChatStreamStatus.execute({ principal: owner, input: { chatId } }) ).rejects.toThrow('Chat not found') diff --git a/apps/sim/lib/mothership/chat/application/read-stream-status.ts b/apps/sim/lib/mothership/chat/application/read-stream-status.ts index f4634c10eac..f5bd1e56e7e 100644 --- a/apps/sim/lib/mothership/chat/application/read-stream-status.ts +++ b/apps/sim/lib/mothership/chat/application/read-stream-status.ts @@ -6,7 +6,10 @@ import { defineWorkspaceOperation } from '@/lib/core/application' import { defineOrganizationOperation } from '@/lib/core/application/organization-operation' import { OrchestrationError } from '@/lib/core/orchestration/types' import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case' -import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context' +import { + requireOwnedChatReadable, + resolveOwnedChatContext, +} from '@/lib/mothership/chat/application/context' import { reconcileChatStreamMarkers } from '@/lib/mothership/chat/stream-liveness' interface ReadChatStreamStatusInput { @@ -44,6 +47,7 @@ export const readChatStreamStatus = defineAuthorizedChatUseCase({ }, authorizationOptions: {}, async execute({ context }) { + await requireOwnedChatReadable(context) const [chat] = await db .select({ conversationId: copilotChats.conversationId }) .from(copilotChats) @@ -51,7 +55,6 @@ export const readChatStreamStatus = defineAuthorizedChatUseCase({ and( eq(copilotChats.id, context.chatId), eq(copilotChats.userId, context.userId), - eq(copilotChats.type, 'mothership'), isNull(copilotChats.deletedAt) ) ) diff --git a/apps/sim/lib/mothership/chat/deletion.ts b/apps/sim/lib/mothership/chat/deletion.ts new file mode 100644 index 00000000000..b816940759a --- /dev/null +++ b/apps/sim/lib/mothership/chat/deletion.ts @@ -0,0 +1,72 @@ +import { db } from '@sim/db' +import { copilotChats } from '@sim/db/schema' +import { and, eq, isNull } from 'drizzle-orm' +import { stopLiveRunsTx } from '@/lib/mothership/async-runs/repository' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' + +interface DeletedChatOwner { + workspaceId: string | null + organizationId: string | null +} + +/** + * Moves a Mothership chat to Recently Deleted and stops its live runs in the same + * transaction, so the chat cannot be deleted while a run keeps working in it and a restore + * cannot revive the run. Null when the chat was not the user's, or was already deleted. + */ +export async function softDeleteMothershipChat(params: { + chatId: string + userId: string +}): Promise { + const deleted = await db.transaction(async (tx) => { + const [chat] = await tx + .update(copilotChats) + .set({ deletedAt: new Date() }) + .where( + and( + eq(copilotChats.id, params.chatId), + eq(copilotChats.userId, params.userId), + eq(copilotChats.type, 'mothership'), + isNull(copilotChats.deletedAt) + ) + ) + .returning({ + workspaceId: copilotChats.workspaceId, + organizationId: copilotChats.organizationId, + }) + if (!chat) return null + return { chat, stoppedRuns: await stopLiveRunsTx(tx, { chatIds: [params.chatId] }) } + }) + if (!deleted) return null + deliverRunStops(deleted.stoppedRuns) + return deleted.chat +} + +/** + * Permanently deletes a copilot chat and stops its live runs in the same transaction. Null + * when the chat was not the user's. + */ +export async function deleteCopilotChat(params: { + chatId: string + userId: string +}): Promise { + const owned = and(eq(copilotChats.id, params.chatId), eq(copilotChats.userId, params.userId)) + const deleted = await db.transaction(async (tx) => { + const [chat] = await tx + .select({ + workspaceId: copilotChats.workspaceId, + organizationId: copilotChats.organizationId, + }) + .from(copilotChats) + .where(owned) + .for('update') + if (!chat) return null + /** Collected before the delete cascades the run rows away. */ + const stoppedRuns = await stopLiveRunsTx(tx, { chatIds: [params.chatId] }) + await tx.delete(copilotChats).where(owned) + return { chat, stoppedRuns } + }) + if (!deleted) return null + deliverRunStops(deleted.stoppedRuns) + return deleted.chat +} diff --git a/apps/sim/lib/mothership/chat/lifecycle.test.ts b/apps/sim/lib/mothership/chat/lifecycle.test.ts index 795d18b6d50..f02fef1fe8b 100644 --- a/apps/sim/lib/mothership/chat/lifecycle.test.ts +++ b/apps/sim/lib/mothership/chat/lifecycle.test.ts @@ -28,7 +28,6 @@ vi.mock('@/lib/mothership/chat/organization-chats', () => mothershipOrganization vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) import { - getAccessibleCopilotChat, getAccessibleCopilotChatAuth, getAccessibleCopilotChatForCancellation, getAccessibleCopilotChatWithMessages, @@ -132,15 +131,6 @@ describe('lifecycle copilot chat reads (cutover to copilot_messages)', () => { expect(dbChainMockFns.orderBy).not.toHaveBeenCalled() }) - it('legacy getAccessibleCopilotChat also assembles messages from copilot_messages', async () => { - dbChainMockFns.limit.mockResolvedValueOnce([{ ...chatRow, model: 'm', config: null }]) - dbChainMockFns.orderBy.mockResolvedValueOnce([{ content: userMsg }]) - - const result = await getAccessibleCopilotChat(CHAT_ID, USER_ID) - - expect(result?.messages).toEqual([userMsg]) - }) - it('scopes the chat lookup to the requesting user, not the chat id alone', async () => { dbChainMockFns.limit.mockResolvedValueOnce([chatRow]) dbChainMockFns.orderBy.mockResolvedValueOnce([]) diff --git a/apps/sim/lib/mothership/chat/lifecycle.ts b/apps/sim/lib/mothership/chat/lifecycle.ts index c094584fab7..bc596ab99b5 100644 --- a/apps/sim/lib/mothership/chat/lifecycle.ts +++ b/apps/sim/lib/mothership/chat/lifecycle.ts @@ -80,18 +80,6 @@ const copilotChatDetailColumns = { updatedAt: copilotChats.updatedAt, } as const -/** - * Column set for the legacy copilot chat detail endpoint. Extends - * `copilotChatDetailColumns` with `model` and `config` — the - * fields the legacy `transformChat` response shape includes. Still drops - * `previewYaml` (JSONB), `pinned`, and `lastSeenAt`. - */ -const copilotChatLegacyDetailColumns = { - ...copilotChatDetailColumns, - model: copilotChats.model, - config: copilotChats.config, -} as const - /** Canonical transcript order; `NULLS LAST` sorts a not-yet-sequenced row after sequenced ones. */ const TRANSCRIPT_ORDER = [ sql`${copilotMessages.seq} asc nulls last`, @@ -305,7 +293,7 @@ type CopilotChatAuthRow = Pick< 'id' | 'userId' | 'workflowId' | 'workspaceId' | 'organizationId' | 'type' > & { mode: ConversationMode } -export type CopilotChatDetail = Pick< +type CopilotChatDetail = Pick< typeof copilotChats.$inferSelect, | 'id' | 'userId' @@ -325,8 +313,35 @@ export type CopilotChatDetailRow = CopilotChatDetail & { messages: unknown[] } -export type CopilotChatLegacyDetailRow = CopilotChatDetailRow & - Pick +/** + * A chat's workflow when the user may read it, or null when the workflow is archived, missing, + * or unreadable. A workflow chat is authorized through its workflow, whose workspace also scopes + * a legacy chat row that carries no workspace of its own. + */ +export async function authorizeChatWorkflowRead( + chatId: string, + userId: string, + workflowId: string +) { + const authorization = await authorizeWorkflowByWorkspacePermission({ + workflowId, + userId, + action: 'read', + }) + if (!authorization.allowed || !authorization.workflow) { + logger.warn('Copilot chat workflow not authorized for user', { chatId, userId, workflowId }) + return null + } + return authorization.workflow +} + +/** A workspace Plan chat is unavailable while Plan mode is off for its owner. */ +export async function isChatModeAvailable( + chat: { organizationId?: string | null; mode: ConversationMode }, + userId: string +): Promise { + return chat.organizationId || chat.mode !== 'plan' ? true : isPlanModeEnabled(userId) +} async function authorizeCopilotChatRow( chat: T | undefined, @@ -360,19 +375,7 @@ async function authorizeCopilotChatRow( throw error } } else if (chat.workflowId) { - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId: chat.workflowId, - userId, - action: 'read', - }) - if (!authorization.allowed || !authorization.workflow) { - logger.warn('Copilot chat workflow not authorized for user', { - chatId, - userId, - workflowId: chat.workflowId, - }) - return null - } + if (!(await authorizeChatWorkflowRead(chatId, userId, chat.workflowId))) return null } else if (chat.workspaceId) { const access = await checkWorkspaceAccess(chat.workspaceId, userId) if (!access.exists || !access.hasAccess) { @@ -386,10 +389,8 @@ async function authorizeCopilotChatRow( } if ( - !chat.organizationId && - chat.mode === 'plan' && organizationAuthorization !== authorizeOrganizationChatCancellation && - !(await isPlanModeEnabled(userId)) + !(await isChatModeAvailable(chat, userId)) ) return null @@ -446,29 +447,6 @@ async function loadAccessibleCopilotChatAuth( return authorizeCopilotChatRow(chat, chatId, userId, principal, organizationAuthorization) } -/** - * Load a copilot chat row for the legacy chat detail endpoint, including the - * transcript plus `model` and `config`. Drops `previewYaml` - * (JSONB), `pinned`, and `lastSeenAt` — none of which the endpoint returns. - */ -export async function getAccessibleCopilotChat( - chatId: string, - userId: string, - options?: { principal?: Principal } -): Promise { - const [chat] = await db - .select(copilotChatLegacyDetailColumns) - .from(copilotChats) - .where(ownedLiveChatWhere(chatId, userId)) - .limit(1) - - const authorized = await authorizeCopilotChatRow(chat, chatId, userId, options?.principal) - if (!authorized) return null - - const messages = await loadCopilotChatMessages(chatId, { projection: 'display' }) - return { ...authorized, messages } -} - /** * Load a copilot chat's detail columns after authorization, without its * transcript. The transcript is unbounded — no per-chat message cap on write @@ -493,20 +471,18 @@ async function getAccessibleCopilotChatDetail( * Load a copilot chat with its transcript (whole, or the `window` asked for) and resources after * authorization, omitting copilot-only TOAST-able fields (`previewYaml`, `config`) and unused * metadata (`model`, `pinned`, `lastSeenAt`). Messages arrive in the chat UI's projection, so - * this serves the mothership chat detail endpoint. A chat whose type differs from `type` is null - * before its transcript is read. + * this serves the chat detail endpoint for mothership and workflow copilot chats alike. */ export async function getAccessibleCopilotChatWithMessages( chatId: string, userId: string, options?: { principal?: Principal - type?: CopilotChatDetail['type'] window?: TranscriptWindow } ): Promise<(CopilotChatDetailRow & Omit) | null> { const chat = await getAccessibleCopilotChatDetail(chatId, userId, options?.principal) - if (!chat || (options?.type && chat.type !== options.type)) return null + if (!chat) return null if (!options?.window) { const messages = await loadCopilotChatMessages(chatId, { projection: 'display' }) diff --git a/apps/sim/lib/mothership/chat/payload.test.ts b/apps/sim/lib/mothership/chat/payload.test.ts index a4d44446f0f..9042d708079 100644 --- a/apps/sim/lib/mothership/chat/payload.test.ts +++ b/apps/sim/lib/mothership/chat/payload.test.ts @@ -24,7 +24,7 @@ import { } from '@sim/testing/mocks/workspace-file-manager.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' import { getExposedIntegrationTools } from '@/lib/integrations/tool-catalog' -import { ChatPayloadSchema } from '@/lib/mothership/generated/protocol' +import { ChatPayloadSchema, TOOL_DEADLINE_CAPABILITY } from '@/lib/mothership/generated/protocol' import { searchUsersV2Tool } from '@/tools/github/search_users' import { gmailListLabelsV2Tool } from '@/tools/gmail/list_labels' import { getToolMetadata } from '@/tools/metadata' @@ -395,6 +395,7 @@ describe('buildCopilotRequestPayload', () => { model: '', }) expect(payload.entitlements).toEqual(enabled ? ['dashboards'] : []) + expect(payload.clientCapabilities).toEqual(['workflow-tool-pickup', TOOL_DEADLINE_CAPABILITY]) } ) @@ -471,7 +472,7 @@ describe('buildCopilotRequestPayload', () => { 'mothershipTools', ]) expect(payload).not.toHaveProperty(key) - expect(payload.clientCapabilities).toEqual([]) + expect(payload.clientCapabilities).toEqual([TOOL_DEADLINE_CAPABILITY]) if (owner === 'organization') expect(payload).not.toHaveProperty('integrationTools') } ) @@ -798,7 +799,7 @@ describe('Assistant payload', () => { terminalCapable: true, }) expect(payload).not.toHaveProperty('desktop') - expect(payload.clientCapabilities).toEqual([]) + expect(payload.clientCapabilities).toEqual([TOOL_DEADLINE_CAPABILITY]) expect(payload.mode).toBe('assistant') expect(payload.assistantSearch).toEqual({ source: 'slack', documentIds: ['document-1'] }) for (const field of ['context', 'commands', 'mothershipTools', 'workflowId']) { diff --git a/apps/sim/lib/mothership/chat/payload.ts b/apps/sim/lib/mothership/chat/payload.ts index 6e43afcdfd9..5054add81d7 100644 --- a/apps/sim/lib/mothership/chat/payload.ts +++ b/apps/sim/lib/mothership/chat/payload.ts @@ -33,7 +33,11 @@ import { buildWorkspaceInventory } from '@/lib/mothership/chat/workspace-invento import { computeEntitlements } from '@/lib/mothership/entitlements' import { isSearchIntegrationToolsEnabled } from '@/lib/mothership/feature-flags' import type { AssistantSearchLevel } from '@/lib/mothership/generated/assistant' -import type { ChatRequest, ModelSelection } from '@/lib/mothership/generated/protocol' +import { + type ChatRequest, + type ModelSelection, + TOOL_DEADLINE_CAPABILITY, +} from '@/lib/mothership/generated/protocol' import type { VfsSnapshotV1 } from '@/lib/mothership/generated/vfs-snapshot-v1' import { getToolEntry } from '@/lib/mothership/tool-executor/router' import { getCopilotToolDescription } from '@/lib/mothership/tools/descriptions' @@ -497,8 +501,12 @@ export async function buildCopilotRequestPayload(params: BuildPayloadParams): Pr } : {}), // The mounted chat view executes client-routed workflow tools (run panel UX), so the - // UI declares that capability explicitly; headless callers omit or send [] and the - // server runs those tools immediately instead of waiting out the pickup grace. - clientCapabilities: isAssistant ? [] : ['workflow-tool-pickup'], + // UI declares that capability explicitly; headless callers omit it and the server runs + // those tools immediately. The deadline token is this server's own: it bounds every + // handed call by the deadline its frame names. + clientCapabilities: [ + ...(isAssistant ? [] : ['workflow-tool-pickup']), + TOOL_DEADLINE_CAPABILITY, + ], } } diff --git a/apps/sim/lib/mothership/chat/persisted-message.ts b/apps/sim/lib/mothership/chat/persisted-message.ts index fbf1a76af8a..cd1a5f34528 100644 --- a/apps/sim/lib/mothership/chat/persisted-message.ts +++ b/apps/sim/lib/mothership/chat/persisted-message.ts @@ -141,7 +141,9 @@ export interface PersistedMessage { * reference when there is nothing to strip, preserving memoized identity for * read-side consumers. */ -export function stripToolResultOutput(message: PersistedMessage): PersistedMessage { +export function stripToolResultOutput>( + message: T +): T { if (!message.contentBlocks?.length) return message let changed = false const contentBlocks = message.contentBlocks.map((block) => { diff --git a/apps/sim/lib/mothership/chat/transcript-pages.integration.ts b/apps/sim/lib/mothership/chat/transcript-pages.integration.ts index 84dded49161..7531a85790e 100644 --- a/apps/sim/lib/mothership/chat/transcript-pages.integration.ts +++ b/apps/sim/lib/mothership/chat/transcript-pages.integration.ts @@ -1,6 +1,13 @@ /** Exercises transcript windows and the stored UI projection against real PostgreSQL. */ import { db } from '@sim/db' -import { copilotChats, copilotMessages, permissions, user, workspace } from '@sim/db/schema' +import { + copilotChats, + copilotMessages, + permissions, + user, + workflow, + workspace, +} from '@sim/db/schema' import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' import { generateId } from '@sim/utils/id' @@ -117,6 +124,37 @@ async function createChat(type: 'mothership' | 'copilot' = 'mothership'): Promis return chat.id } +/** + * A workflow Copilot panel chat, as the panel stores it. A legacy row, from before chats carried + * a workspace, has only its workflow's. + */ +async function createWorkflowChat( + options: { archivedWorkflow?: boolean; legacyRow?: boolean } = {} +): Promise { + const workflowId = generateId() + const now = new Date() + await db.insert(workflow).values({ + id: workflowId, + userId: ownerId, + workspaceId, + name: `Transcript pages workflow ${workflowId}`, + lastSynced: now, + createdAt: now, + updatedAt: now, + archivedAt: options.archivedWorkflow ? now : null, + }) + const [chat] = await db + .insert(copilotChats) + .values({ + userId: ownerId, + workflowId, + workspaceId: options.legacyRow ? null : workspaceId, + type: 'copilot', + }) + .returning({ id: copilotChats.id }) + return chat.id +} + async function appendTurns(chatId: string, turns: number, firstTurn = 0): Promise { const messages: PersistedMessage[] = [] for (let turn = firstTurn; turn < firstTurn + turns; turn++) { @@ -334,22 +372,7 @@ describe('chat transcript pages in PostgreSQL', () => { ) }) - it('reads a transcript only for a chat of the requested type', async () => { - const copilotChatId = await createChat('copilot') - const mothershipChatId = await createChat('mothership') - await appendTurns(copilotChatId, 1) - await appendTurns(mothershipChatId, 1) - - expect( - await getAccessibleCopilotChatWithMessages(copilotChatId, ownerId, { type: 'mothership' }) - ).toBeNull() - const mothership = await getAccessibleCopilotChatWithMessages(mothershipChatId, ownerId, { - type: 'mothership', - }) - expect(mothership?.messages).toHaveLength(2) - }) - - it("serves older pages only to the chat's owner, only for mothership chats and issued cursors", async () => { + it("serves older pages only to the chat's owner and only for issued cursors", async () => { const chatId = await createChat() await appendTurns(chatId, 40) const before = (await latestWindow(chatId))?.olderCursor @@ -364,12 +387,34 @@ describe('chat transcript pages in PostgreSQL', () => { await expect( readChatOlderMessages.execute({ principal: outsider, input: { chatId, before } }) ).rejects.toThrow('Chat not found') - const copilotChatId = await createChat('copilot') - await expect( - readChatOlderMessages.execute({ principal: owner, input: { chatId: copilotChatId, before } }) - ).rejects.toThrow('Chat not found') await expect( readChatOlderMessages.execute({ principal: owner, input: { chatId, before: 'tampered' } }) ).rejects.toThrow('Invalid cursor') }) + + it('serves older pages of a workflow copilot chat while its owner may read the workflow', async () => { + const chatId = await createWorkflowChat() + const legacyChatId = await createWorkflowChat({ legacyRow: true }) + const archivedChatId = await createWorkflowChat({ archivedWorkflow: true }) + for (const id of [chatId, legacyChatId, archivedChatId]) await appendTurns(id, 40) + + for (const id of [chatId, legacyChatId]) { + const cursor = (await latestWindow(id))?.olderCursor + if (!cursor) throw new Error('latest window reaches the first message') + const page = await readChatOlderMessages.execute({ + principal: owner, + input: { chatId: id, before: cursor }, + }) + expect(page.messages.length).toBeGreaterThan(0) + } + + const before = (await latestWindow(chatId))?.olderCursor + if (!before) throw new Error('latest window reaches the first message') + await expect( + readChatOlderMessages.execute({ principal: outsider, input: { chatId, before } }) + ).rejects.toThrow('Chat not found') + await expect( + readChatOlderMessages.execute({ principal: owner, input: { chatId: archivedChatId, before } }) + ).rejects.toThrow('Chat not found') + }) }) diff --git a/apps/sim/lib/mothership/content-delivery/registry-gate.test.ts b/apps/sim/lib/mothership/content-delivery/registry-gate.test.ts index 2eea660c1d7..67abfb84e8c 100644 --- a/apps/sim/lib/mothership/content-delivery/registry-gate.test.ts +++ b/apps/sim/lib/mothership/content-delivery/registry-gate.test.ts @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/core/security/encryption', () => encryptionMock) const telemetry = vi.hoisted(() => ({ reportDurableSecretProvenanceRefusal: vi.fn(), + reportDurableSecretProvenanceRefusals: vi.fn(), reportDurableSecretProvenanceUnrecordedBatch: vi.fn(), })) vi.mock('@/lib/execution/durable-secret-provenance-telemetry', () => telemetry) @@ -120,19 +121,33 @@ describe('createRegistryContentGate', () => { it('refuses an opaque view of an item holding secrets, which no redaction reaches', async () => { const registry = newRegistry() const gate = createRegistryContentGate({ registry, actorUserId: 'user-1' }) - expect(await gate.admit([item('archive', exact(SECRET), 'opaque')])).toEqual([false]) + expect(await gate.admit([item('archive', exact(SECRET), 'opaque')])).toEqual(['opaque']) expect(registry.isPermanentlyIncomplete()).toBe(false) - expect(telemetry.reportDurableSecretProvenanceRefusal).toHaveBeenCalledWith( + expect(telemetry.reportDurableSecretProvenanceRefusals).toHaveBeenCalledWith([ expect.objectContaining({ resourceId: 'archive', cause: 'workspace-file-opaque-secret-content', - }) + }), + ]) + }) + + it('reports every item one admission refuses as one batch, not one line per item', async () => { + telemetry.reportDurableSecretProvenanceRefusals.mockClear() + const gate = createRegistryContentGate({ registry: newRegistry(), actorUserId: 'user-1' }) + const refused = Array.from({ length: 50 }, (_, index) => + item(`legacy-${index}`, { status: 'unknown' }) ) + const verdicts = await gate.admit([...refused, item('clean', exact())]) + expect(verdicts).toEqual([...refused.map(() => 'unknown'), true]) + expect(telemetry.reportDurableSecretProvenanceRefusals).toHaveBeenCalledTimes(1) + const [reports] = telemetry.reportDurableSecretProvenanceRefusals.mock.calls[0] + expect(reports).toHaveLength(50) + expect(telemetry.reportDurableSecretProvenanceRefusal).not.toHaveBeenCalled() }) it('refuses an item holding secrets when there is no registry to redact with', async () => { const gate = createRegistryContentGate({ actorUserId: 'user-1' }) - expect(await gate.admit([item('config', exact(SECRET))])).toEqual([false]) + expect(await gate.admit([item('config', exact(SECRET))])).toEqual(['registry']) }) it('tracks opaque workbench downloads without relaxing model or excerpt admission', async () => { @@ -144,18 +159,18 @@ describe('createRegistryContentGate', () => { }) expect(await gate.admit([item('download', exact(SECRET), 'opaque')])).toEqual([true]) expect(modelView(SECRET, registry)).not.toContain(SECRET) - expect(await gate.admit([item('snippet', exact(SECRET), 'excerpt')])).toEqual([false]) + expect(await gate.admit([item('snippet', exact(SECRET), 'excerpt')])).toEqual(['opaque']) expect( await createRegistryContentGate({ actorUserId: 'user-1', destination: 'workbench' }).admit([ item('download', exact(SECRET), 'opaque'), ]) - ).toEqual([false]) + ).toEqual(['registry']) }) it('refuses an unknown item without latching the registry', async () => { const registry = newRegistry() const gate = createRegistryContentGate({ registry, actorUserId: 'user-1' }) - expect(await gate.admit([item('tainted', { status: 'unknown' })])).toEqual([false]) + expect(await gate.admit([item('tainted', { status: 'unknown' })])).toEqual(['unknown']) expect(registry.isPermanentlyIncomplete()).toBe(false) }) @@ -169,7 +184,7 @@ describe('createRegistryContentGate', () => { it('refuses an unknown item under taint when no registry could withhold the output', async () => { const gate = createRegistryContentGate({ actorUserId: 'user-1', unknown: 'taint' }) - expect(await gate.admit([item('tainted', { status: 'unknown' })])).toEqual([false]) + expect(await gate.admit([item('tainted', { status: 'unknown' })])).toEqual(['unknown']) }) it('refuses only the item whose secrets cannot be imported, keeping the rest of the call', async () => { @@ -184,7 +199,7 @@ describe('createRegistryContentGate', () => { item('config', exact(SECRET)), item('clean', { status: 'exact', entries: [] }), ]) - expect(verdicts).toEqual([false, true, true]) + expect(verdicts).toEqual(['import', true, true]) expect(registry.isPermanentlyIncomplete()).toBe(false) expect(modelView(`token=${SECRET}`, registry)).not.toContain(SECRET) }) @@ -198,7 +213,7 @@ describe('createRegistryContentGate', () => { const gate = createRegistryContentGate({ registry, actorUserId: 'user-1' }) expect(await gate.admit([item('existing', exact(...values))])).toEqual([true]) - expect(await gate.admit([item('overflow', exact(SECRET, OTHER_SECRET))])).toEqual([false]) + expect(await gate.admit([item('overflow', exact(SECRET, OTHER_SECRET))])).toEqual(['import']) expect(registry.isPermanentlyIncomplete()).toBe(false) expect(modelView(`public notes token=${values[0]}`, registry)).toBe( @@ -221,7 +236,7 @@ describe('createRegistryContentGate', () => { gate.admit([item('second', exact(OTHER_SECRET))]), ]) - expect(verdicts.flat().sort()).toEqual([false, true]) + expect(verdicts.flat().sort()).toEqual(['import', true]) expect(registry.isPermanentlyIncomplete()).toBe(false) expect(modelView(`public notes token=${values[0]}`, registry)).toBe( 'public notes token={{FIXTURE_TOKEN}}' diff --git a/apps/sim/lib/mothership/content-delivery/registry-gate.ts b/apps/sim/lib/mothership/content-delivery/registry-gate.ts index ba6bd14fa90..3937b3d1ae2 100644 --- a/apps/sim/lib/mothership/content-delivery/registry-gate.ts +++ b/apps/sim/lib/mothership/content-delivery/registry-gate.ts @@ -1,4 +1,4 @@ -import type { ContentDeliveryGate, ContentSource } from '@/lib/core/application' +import type { ContentDeliveryGate, ContentSource, ContentVerdict } from '@/lib/core/application' import { importDurableSecretProvenance } from '@/lib/execution/durable-secret-provenance' import { type DurableSecretProvenanceUnrecordedReport, @@ -6,8 +6,8 @@ import { } from '@/lib/execution/durable-secret-provenance-telemetry' import { CONTENT_SURFACE, - type ContentRefusalReason, - reportContentRefusal, + type ContentRefusals, + createContentRefusals, } from '@/lib/mothership/content-delivery/telemetry' import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' @@ -33,13 +33,11 @@ export interface RegistryContentGateScope { * any entry, so a refused item cannot latch the registry for every other item. */ export function createRegistryContentGate(scope: RegistryContentGateScope): ContentDeliveryGate { - const refuse = (source: ContentSource, reason: ContentRefusalReason): false => - reportContentRefusal(source, reason, scope.workspaceId) - const decide = async ( source: ContentSource, - unrecorded: DurableSecretProvenanceUnrecordedReport[] - ): Promise => { + unrecorded: DurableSecretProvenanceUnrecordedReport[], + { refuse }: ContentRefusals + ): Promise => { const { provenance } = source if (provenance.status === 'unknown') { /** Taint needs a registry to latch; without one the bytes would reach the model as is. */ @@ -89,8 +87,13 @@ export function createRegistryContentGate(scope: RegistryContentGateScope): Cont }, async admit(sources) { const unrecorded: DurableSecretProvenanceUnrecordedReport[] = [] - const verdicts: boolean[] = [] - for (const source of sources) verdicts.push(await decide(source, unrecorded)) + const refusals = createContentRefusals(scope.workspaceId) + const verdicts: ContentVerdict[] = [] + try { + for (const source of sources) verdicts.push(await decide(source, unrecorded, refusals)) + } finally { + refusals.report() + } reportDurableSecretProvenanceUnrecordedBatch(unrecorded) return verdicts }, diff --git a/apps/sim/lib/mothership/content-delivery/session-gate.test.ts b/apps/sim/lib/mothership/content-delivery/session-gate.test.ts index 164c2646bb7..6b664b7c48f 100644 --- a/apps/sim/lib/mothership/content-delivery/session-gate.test.ts +++ b/apps/sim/lib/mothership/content-delivery/session-gate.test.ts @@ -8,6 +8,7 @@ const history = vi.hoisted(() => ({ recordExistingSessionFileInput: vi.fn() })) vi.mock('@/lib/execution/remote-sandbox/session-file-provenance', () => history) const telemetry = vi.hoisted(() => ({ reportDurableSecretProvenanceRefusal: vi.fn(), + reportDurableSecretProvenanceRefusals: vi.fn(), reportDurableSecretProvenanceUnrecordedBatch: vi.fn(), })) vi.mock('@/lib/execution/durable-secret-provenance-telemetry', () => telemetry) @@ -42,11 +43,23 @@ beforeEach(() => { describe('createSessionContentGate', () => { it('keeps an unknown item out of the machine and its history', async () => { - expect(await gate().admit([item('tainted', { status: 'unknown' })])).toEqual([false]) + expect(await gate().admit([item('tainted', { status: 'unknown' })])).toEqual(['unknown']) expect(history.recordExistingSessionFileInput).not.toHaveBeenCalled() - expect(telemetry.reportDurableSecretProvenanceRefusal).toHaveBeenCalledWith( - expect.objectContaining({ resourceId: 'tainted' }) + expect(telemetry.reportDurableSecretProvenanceRefusals).toHaveBeenCalledWith([ + expect.objectContaining({ resourceId: 'tainted' }), + ]) + }) + + it('reports every item one admission refuses as one batch, not one line per item', async () => { + telemetry.reportDurableSecretProvenanceRefusals.mockClear() + const refused = Array.from({ length: 50 }, (_, index) => + item(`tainted-${index}`, { status: 'unknown' }) ) + expect(await gate().admit(refused)).toEqual(refused.map(() => 'unknown')) + expect(telemetry.reportDurableSecretProvenanceRefusals).toHaveBeenCalledTimes(1) + const [reports] = telemetry.reportDurableSecretProvenanceRefusals.mock.calls[0] + expect(reports).toHaveLength(50) + expect(telemetry.reportDurableSecretProvenanceRefusal).not.toHaveBeenCalled() }) it('admits secret-bearing bytes, even opaque, after merging their entries into the history', async () => { @@ -55,7 +68,7 @@ describe('createSessionContentGate', () => { item('tainted', { status: 'unknown' }), item('clean', { status: 'exact', entries: [] }), ]) - expect(verdicts).toEqual([true, false, true]) + expect(verdicts).toEqual([true, 'unknown', true]) expect(history.recordExistingSessionFileInput).toHaveBeenCalledTimes(1) expect(history.recordExistingSessionFileInput).toHaveBeenCalledWith('session-1', { status: 'exact', diff --git a/apps/sim/lib/mothership/content-delivery/session-gate.ts b/apps/sim/lib/mothership/content-delivery/session-gate.ts index a5fc9e387f3..89f6b1c439e 100644 --- a/apps/sim/lib/mothership/content-delivery/session-gate.ts +++ b/apps/sim/lib/mothership/content-delivery/session-gate.ts @@ -1,4 +1,4 @@ -import type { ContentDeliveryGate } from '@/lib/core/application' +import type { ContentDeliveryGate, ContentVerdict } from '@/lib/core/application' import { createDurableSecretProvenanceRegistry, type DurableSecretProvenance, @@ -7,7 +7,7 @@ import { } from '@/lib/execution/durable-secret-provenance' import { reportDurableSecretProvenanceUnrecordedBatch } from '@/lib/execution/durable-secret-provenance-telemetry' import { recordExistingSessionFileInput } from '@/lib/execution/remote-sandbox/session-file-provenance' -import { reportContentRefusal } from '@/lib/mothership/content-delivery/telemetry' +import { createContentRefusals } from '@/lib/mothership/content-delivery/telemetry' import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' /** The workbench's history could not be widened, so nothing may enter the machine. */ @@ -53,13 +53,14 @@ export function createSessionContentGate(scope: { } }, async admit(sources) { - const verdicts: boolean[] = [] + const verdicts: ContentVerdict[] = [] const admitted: DurableSecretProvenance[] = [] + const refusals = createContentRefusals(scope.workspaceId) let unrecorded = 0 for (const source of sources) { const { provenance } = source if (provenance.status === 'unknown') { - verdicts.push(reportContentRefusal(source, 'unknown', scope.workspaceId)) + verdicts.push(refusals.refuse(source, 'unknown')) continue } if ( @@ -67,7 +68,7 @@ export function createSessionContentGate(scope: { provenance.entries.length > 0 && source.view === 'excerpt' ) { - verdicts.push(reportContentRefusal(source, 'opaque', scope.workspaceId)) + verdicts.push(refusals.refuse(source, 'opaque')) continue } if (provenance.status === 'unrecorded') { @@ -78,6 +79,7 @@ export function createSessionContentGate(scope: { } verdicts.push(true) } + refusals.report() if (admitted.length > 0) { try { await recordExistingSessionFileInput( diff --git a/apps/sim/lib/mothership/content-delivery/telemetry.test.ts b/apps/sim/lib/mothership/content-delivery/telemetry.test.ts new file mode 100644 index 00000000000..b70a864cbf7 --- /dev/null +++ b/apps/sim/lib/mothership/content-delivery/telemetry.test.ts @@ -0,0 +1,40 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { ContentRefusalReason, ContentSourceKind } from '@/lib/core/application' +import { describeWithheldContent } from '@/lib/mothership/content-delivery/telemetry' + +function withheld( + entries: [ContentSourceKind, [ContentRefusalReason, number][]][] +): Map> { + return new Map(entries.map(([kind, reasons]) => [kind, new Map(reasons)])) +} + +describe('describeWithheldContent', () => { + it('says nothing when nothing was withheld', () => { + expect(describeWithheldContent(new Map())).toBeUndefined() + }) + + it('tells Chat what each refusal means for its next read, naming no item', () => { + const note = describeWithheldContent( + withheld([ + ['knowledge-document', [['unknown', 3]]], + ['workspace-file', [['opaque', 1]]], + [ + 'table-row', + [ + ['registry', 2], + ['import', 1], + ], + ], + ]) + ) + expect(note?.split('\n')).toEqual([ + 'Note: 3 knowledge documents were withheld from this result: their secret provenance is unknown, so reading them again will not return them. They exist; do not treat them as missing.', + 'Note: 1 file was withheld from this result: it contains recorded secrets and cannot be shown as raw bytes or a preview. Read it as text for a redacted copy.', + 'Note: 2 table rows were withheld from this result: their secret provenance could not be verified right now. They may be readable on a later request.', + 'Note: 1 table row was withheld from this result: its secret provenance could not be verified right now. It may be readable on a later request.', + ]) + }) +}) diff --git a/apps/sim/lib/mothership/content-delivery/telemetry.ts b/apps/sim/lib/mothership/content-delivery/telemetry.ts index 658aee2a55e..d3946026e40 100644 --- a/apps/sim/lib/mothership/content-delivery/telemetry.ts +++ b/apps/sim/lib/mothership/content-delivery/telemetry.ts @@ -1,12 +1,16 @@ -import type { ContentSource, ContentSourceKind } from '@/lib/core/application' +import type { + ContentRefusalReason, + ContentSource, + ContentSourceKind, + WithheldContent, +} from '@/lib/core/application' import { type DurableSecretProvenanceRefusalCause, + type DurableSecretProvenanceRefusalReport, type DurableSecretProvenanceSurface, - reportDurableSecretProvenanceRefusal, + reportDurableSecretProvenanceRefusals, } from '@/lib/execution/durable-secret-provenance-telemetry' -export type ContentRefusalReason = 'unknown' | 'opaque' | 'registry' | 'import' - export const CONTENT_SURFACE: Record = { 'workspace-file': 'workspace-file', 'execution-file': 'workspace-file', @@ -55,17 +59,78 @@ const CAUSES: Record< }, } -/** Names the refused item and why, never its content. */ -export function reportContentRefusal( - source: ContentSource, - reason: ContentRefusalReason, - workspaceId: string | undefined -): false { - reportDurableSecretProvenanceRefusal({ - surface: CONTENT_SURFACE[source.kind], - cause: CAUSES[source.kind][reason], - workspaceId, - resourceId: source.id, - }) - return false +/** + * Collects the items one admission refuses and reports them once per surface and cause, naming + * each item by its id and why, never its content. A page of legacy rows is one line. + */ +export interface ContentRefusals { + /** Records the refusal of `source`; returns the verdict, its reason. */ + refuse(source: ContentSource, reason: ContentRefusalReason): ContentRefusalReason + /** Reports what was recorded. */ + report(): void +} + +export function createContentRefusals(workspaceId: string | undefined): ContentRefusals { + const reports: DurableSecretProvenanceRefusalReport[] = [] + return { + refuse(source, reason) { + reports.push({ + surface: CONTENT_SURFACE[source.kind], + cause: CAUSES[source.kind][reason], + workspaceId, + resourceId: source.id, + }) + return reason + }, + report(): void { + reportDurableSecretProvenanceRefusals(reports) + }, + } +} + +const WITHHELD_LABELS: Record = { + 'workspace-file': ['file', 'files'], + 'execution-file': ['run file', 'run files'], + 'knowledge-document': ['knowledge document', 'knowledge documents'], + 'knowledge-chunk': ['knowledge chunk', 'knowledge chunks'], + 'table-row': ['table row', 'table rows'], + 'tool-result': ['tool result', 'tool results'], +} + +/** What each refusal means for Chat's next read, as `[one item, several items]`. */ +const WITHHELD_REASONS: Record = { + unknown: [ + 'its secret provenance is unknown, so reading it again will not return it. It exists; do not treat it as missing.', + 'their secret provenance is unknown, so reading them again will not return them. They exist; do not treat them as missing.', + ], + opaque: [ + 'it contains recorded secrets and cannot be shown as raw bytes or a preview. Read it as text for a redacted copy.', + 'they contain recorded secrets and cannot be shown as raw bytes or a preview. Read them as text for a redacted copy.', + ], + registry: [ + 'its secret provenance could not be verified right now. It may be readable on a later request.', + 'their secret provenance could not be verified right now. They may be readable on a later request.', + ], + import: [ + 'its secret provenance could not be verified right now. It may be readable on a later request.', + 'their secret provenance could not be verified right now. They may be readable on a later request.', + ], +} + +/** + * Tells Chat's model what a result left out, and what to do about it: a refused item is dropped + * from the page it was read in, which otherwise reads exactly like an item that does not exist, + * and a model that cannot tell why keeps reading it again. Names no item. + */ +export function describeWithheldContent(withheld: WithheldContent): string | undefined { + const lines: string[] = [] + for (const [kind, reasons] of withheld) { + for (const [reason, count] of reasons) { + const form = count === 1 ? 0 : 1 + lines.push( + `Note: ${count} ${WITHHELD_LABELS[kind][form]} ${count === 1 ? 'was' : 'were'} withheld from this result: ${WITHHELD_REASONS[reason][form]}` + ) + } + } + return lines.length > 0 ? lines.join('\n') : undefined } diff --git a/apps/sim/lib/mothership/generated/billing.ts b/apps/sim/lib/mothership/generated/billing.ts index abdec1159ad..cf01b6b4b98 100644 --- a/apps/sim/lib/mothership/generated/billing.ts +++ b/apps/sim/lib/mothership/generated/billing.ts @@ -90,6 +90,8 @@ export const UsageLimitRefusal = z.object({ usageUpgrade: UsageUpgrade.optional().catch(undefined), }); export const BillingDuplicateCode = "DUPLICATE_BILLING_EVENT"; +/** A 503: another attempt of the same request is still being applied, so this one's outcome is unknown. */ +export const BillingInFlightCode = "BILLING_REQUEST_IN_FLIGHT"; /** Sim authors receipts at the paid execution boundary; tool output carries no billing authority. */ export const ServiceUsageReceipt = z.strictObject({ diff --git a/apps/sim/lib/mothership/generated/computer-use.ts b/apps/sim/lib/mothership/generated/computer-use.ts index 203e63b80d0..71f908ed036 100644 --- a/apps/sim/lib/mothership/generated/computer-use.ts +++ b/apps/sim/lib/mothership/generated/computer-use.ts @@ -17,6 +17,16 @@ const SnapshotId = z .describe("Latest unconsumed snapshotId from get_app_state for this app. Actions consume it."); const ElementId = z.string().min(1).max(128).describe("Element reference from that snapshot."); const WindowId = z.string().min(1).max(128).describe("Window reference from that snapshot."); +/** Input fields shared across actions keep one description: Anthropic flattens the union and anyOfs differing copies. */ +const TargetWindowId = WindowId.optional().describe( + "Window reference from an earlier snapshot of this app. If omitted, get_app_state and press_key use the app's focused window; coordinate actions use the window the snapshot selected.", +); +const IncludeScreenshot = z + .boolean() + .optional() + .describe( + "Set true to include a screenshot in the returned state. On actions it overrides observeAfter.includeScreenshot.", + ); const Coordinate = z.number().nonnegative().max(100_000); const ObserveAfter = { observeAfter: z @@ -25,14 +35,20 @@ const ObserveAfter = { .describe( "Every mutation returns fresh app state by default. Set includeScreenshot to true to include an image; otherwise no screenshot is captured. Observation failure never retries the action.", ), + includeScreenshot: IncludeScreenshot, }; const InputSequenceStep = z.discriminatedUnion("action", [ z.strictObject({ action: z.literal("type_text"), text: z.string().max(32_000) }), z.strictObject({ action: z.literal("press_key"), key: z.string().min(1).max(128) }), ]); const ElementTarget = { bundleId: BundleId, snapshotId: SnapshotId, elementId: ElementId }; -const WindowTarget = { bundleId: BundleId, snapshotId: SnapshotId, windowId: WindowId }; -const PointTarget = { ...WindowTarget, x: Coordinate, y: Coordinate }; +const PointTarget = { + bundleId: BundleId, + snapshotId: SnapshotId, + windowId: TargetWindowId, + x: Coordinate, + y: Coordinate, +}; const ClickOptions = { button: z.enum(["left", "right"]).optional(), clickCount: z.number().int().min(1).max(3).optional(), @@ -42,14 +58,23 @@ const ScrollOffsets = { .number() .min(-10_000) .max(10_000) - .describe("Horizontal scroll distance in points; positive scrolls right, negative left."), + .optional() + .describe( + "Horizontal scroll distance in points; positive scrolls right, negative left. Omit for 0 when deltaY is set.", + ), deltaY: z .number() .min(-10_000) .max(10_000) - .describe("Vertical scroll distance in points; positive scrolls down, negative up."), + .optional() + .describe( + "Vertical scroll distance in points; positive scrolls down, negative up. Omit for 0 when deltaX is set.", + ), }; +const hasScrollDistance = (input: { deltaX?: number | undefined; deltaY?: number | undefined }) => + input.deltaX !== undefined || input.deltaY !== undefined; + /** Strict branches require an observed, unambiguous target before native input. */ export const ComputerUseSchema = z.union([ z.strictObject({ action: z.literal("status") }), @@ -58,11 +83,8 @@ export const ComputerUseSchema = z.union([ z.strictObject({ action: z.literal("get_app_state"), bundleId: BundleId, - windowId: WindowId.optional(), - includeScreenshot: z - .boolean() - .optional() - .describe("Set true to include an image of the selected window."), + windowId: TargetWindowId, + includeScreenshot: IncludeScreenshot, }), z.strictObject({ action: z.literal("click"), ...ElementTarget, ...ObserveAfter, ...ClickOptions }), z.strictObject({ action: z.literal("click"), ...PointTarget, ...ObserveAfter, ...ClickOptions }), @@ -75,11 +97,24 @@ export const ComputerUseSchema = z.union([ z.strictObject({ action: z.literal("press_key"), ...ObserveAfter, - ...WindowTarget, - key: z.string().min(1).max(128).describe("One key or chord, such as Enter, Tab, Escape, or Cmd+A."), + bundleId: BundleId, + snapshotId: SnapshotId, + windowId: TargetWindowId, + elementId: ElementId.optional(), + key: z + .string() + .min(1) + .max(128) + .describe( + "One key or chord, such as Enter, Tab, Escape, or Cmd+A. With elementId, focuses that element first.", + ), }), - z.strictObject({ action: z.literal("scroll"), ...ElementTarget, ...ObserveAfter, ...ScrollOffsets }), - z.strictObject({ action: z.literal("scroll"), ...PointTarget, ...ObserveAfter, ...ScrollOffsets }), + z + .strictObject({ action: z.literal("scroll"), ...ElementTarget, ...ObserveAfter, ...ScrollOffsets }) + .refine(hasScrollDistance, "Scroll needs deltaX or deltaY."), + z + .strictObject({ action: z.literal("scroll"), ...PointTarget, ...ObserveAfter, ...ScrollOffsets }) + .refine(hasScrollDistance, "Scroll needs deltaX or deltaY."), z.strictObject({ action: z.literal("drag"), ...PointTarget, diff --git a/apps/sim/lib/mothership/generated/memory-notes-access.ts b/apps/sim/lib/mothership/generated/memory-notes-access.ts new file mode 100644 index 00000000000..b1c414e4986 --- /dev/null +++ b/apps/sim/lib/mothership/generated/memory-notes-access.ts @@ -0,0 +1,59 @@ +// GENERATED — do not edit. Source of truth: mothership worker packages/contracts/src/memory-notes-access.ts +// Regenerate with `bun run contracts:sync` in the worker. + +import { z } from "zod"; + +// Answers tolerate fields a newer peer adds; requests stay strict. +const permissionsSchema = z.object({ + read: z.literal(true), + privateMutation: z.literal(true), + sharedMutation: z.boolean(), +}); +const projectSchema = z.object({ + scope: z.literal("project"), + id: z.uuid(), + name: z.string(), + permissions: permissionsSchema, +}); + +export const MemoryNotesAccessInput = z.strictObject({ + target: z.discriminatedUnion("scope", [ + z.strictObject({ scope: z.literal("personal") }), + z.strictObject({ scope: z.literal("workspace"), workspaceId: z.uuid() }), + z.strictObject({ scope: z.literal("project"), projectId: z.uuid() }), + ]), + action: z.enum(["read", "private_mutation", "shared_mutation"]), +}); +export type MemoryNotesAccessInput = z.infer; + +export const MemoryNotesAccessResult = z.object({ + success: z.literal(true), + version: z.literal(1), + actor: z.object({ userId: z.string().min(1), chatId: z.uuid() }), + target: z.discriminatedUnion("scope", [ + z.object({ + scope: z.literal("personal"), + permissions: permissionsSchema.extend({ sharedMutation: z.literal(false) }), + }), + projectSchema, + z.object({ + scope: z.literal("workspace"), + id: z.uuid(), + name: z.string(), + permissions: permissionsSchema, + currentProject: projectSchema.nullable(), + }), + ]), +}); +export type MemoryNotesAccessResult = z.infer; + +export const MemoryCleanupInput = z.strictObject({ + version: z.literal(1), + target: z.discriminatedUnion("kind", [ + z.strictObject({ kind: z.literal("private_owner"), userId: z.string().min(1).max(200) }), + z.strictObject({ kind: z.literal("workspace"), workspaceId: z.uuid() }), + z.strictObject({ kind: z.literal("project"), projectId: z.uuid() }), + ]), +}); +export type MemoryCleanupInput = z.infer; +export const MemoryCleanupResult = z.object({ success: z.literal(true) }); diff --git a/apps/sim/lib/mothership/generated/protocol.ts b/apps/sim/lib/mothership/generated/protocol.ts index e39249bc3a5..8705d4f1c96 100644 --- a/apps/sim/lib/mothership/generated/protocol.ts +++ b/apps/sim/lib/mothership/generated/protocol.ts @@ -274,6 +274,27 @@ export interface StreamToolReplay { workspaceId?: string | undefined; } +/** + * How long the worker waits for one handed tool call's result, measured from the hand-off. + * Sim converts it to an absolute deadline on receipt, persists it with the call (a takeover + * re-run inherits what remains) and settles the call, cancellation included, by + * `deadlineMs - TOOL_DEADLINE_SETTLE_MARGIN_MS`: approval wait, pickup, execution and lease + * extensions all fit inside it. Sim policy may only end a call sooner. Absent = the + * receiver's own bounds. + */ +export interface StreamToolDeadline { + deadlineMs?: number | undefined; +} + +/** + * Room between Sim settling a call and the worker's own bound for it: Sim's resume-gate + * grace plus the time its result takes to reach the worker. + */ +export const TOOL_DEADLINE_SETTLE_MARGIN_MS = 90_000; + +/** The `clientCapabilities` token a Sim sends when it honors `StreamToolDeadline`. */ +export const TOOL_DEADLINE_CAPABILITY = "tool-deadline"; + /** POST /api/mothership — the chat request sim sends. */ export interface ChatRequest extends StreamResponseReceipt { desktop?: DesktopContext | undefined; @@ -315,7 +336,8 @@ export interface ChatRequest extends StreamResponseReceipt { * empty array means "I pick up nothing", so sim-side dispatch must skip client-pickup * grace windows and run tools server-side immediately. ABSENT = older/unknown caller — * dispatch keeps its conservative grace (deploy-skew safe: a stale tab that predates - * this field still gets waited on). Known capability: "workflow-tool-pickup". + * this field still gets waited on). Known capability: "workflow-tool-pickup". Sim also + * declares its own {@link TOOL_DEADLINE_CAPABILITY} here. */ clientCapabilities?: string[] | undefined; /** diff --git a/apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts b/apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts index f09e343936b..2fe72fdb685 100644 --- a/apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts +++ b/apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts @@ -424,12 +424,26 @@ export const listWorkspacesResultSchema = z.object({ nextCursor: z.uuid().nullable(), storedNotes: z .array( - z.object({ - id: z.string(), - kind: z.enum(['user_pref', 'workspace_fact']), - content: z.string(), - updatedAt: z.iso.datetime(), - }) + z + .object({ + id: z.string(), + content: z.string(), + updatedAt: z.iso.datetime(), + by: z.enum(['this user', 'another workspace member', 'another user']).optional(), + chat: z.enum(['this chat', 'another chat']).optional(), + }) + .and( + z.union([ + z.object({ kind: z.enum(['user_pref', 'workspace_fact']) }), + z.object({ + scope: z.enum(['personal', 'project', 'workspace']), + visibility: z.enum(['private', 'shared']), + projectId: z.uuid().nullable(), + workspaceId: z.uuid().nullable(), + revision: z.number().int().positive(), + }), + ]) + ) ) .optional(), }) diff --git a/apps/sim/lib/mothership/generated/sim-transport.ts b/apps/sim/lib/mothership/generated/sim-transport.ts index 3e3604c8cae..c0b8695bb35 100644 --- a/apps/sim/lib/mothership/generated/sim-transport.ts +++ b/apps/sim/lib/mothership/generated/sim-transport.ts @@ -3,13 +3,16 @@ import { z } from "zod"; import { IntegrationCatalogRequest } from "./integration-catalog"; +import { MemoryNotesAccessInput } from "./memory-notes-access"; import { MemoryScopeRequest } from "./memory-scope"; import { RunControlRequest } from "./run-control"; import { TaskWakeRequest, WorkflowWatchRequest } from "./tasks"; +const SimChannelId = z.string().regex(/^[a-f0-9]{64}$/); + export const SimConnection = z.discriminatedUnion("mode", [ z.object({ mode: z.literal("direct") }), - z.object({ mode: z.literal("checkpoint"), channelId: z.string().regex(/^[a-f0-9]{64}$/) }), + z.object({ mode: z.literal("checkpoint"), channelId: SimChannelId }), ]); export type SimConnection = z.infer; @@ -27,6 +30,7 @@ export type SimScope = z.infer; /** Only idempotent controls use the idle channel; tool effects use the run event log. */ export const SimControlOperation = z.discriminatedUnion("kind", [ + z.object({ kind: z.literal("memory_notes_access"), input: MemoryNotesAccessInput }), z.object({ kind: z.literal("memory_scope"), input: MemoryScopeRequest }), z.object({ kind: z.literal("run_control"), input: RunControlRequest }), z.object({ kind: z.literal("workflow_status"), input: WorkflowWatchRequest }), @@ -51,9 +55,43 @@ export const SimControlResult = z.object({ }); export type SimControlResult = z.infer; -export const SimChannelPoll = z.object({ channelId: z.string().regex(/^[a-f0-9]{64}$/) }); +export type SimControlKind = SimControlOperation["kind"]; +/** Every kind this build's union parses: what a receiver declares in `accepts`. */ +export const SIM_CONTROL_KINDS: SimControlKind[] = SimControlOperation.options.map( + (option) => option.shape.kind.value, +); + +/** The v1.0.0 kinds every receiver parses without declaring them. Frozen: a poll without + * `accepts` comes from a receiver that knows only these, so every later kind is negotiated. */ +export const SIM_CONTROL_BASELINE_KINDS = [ + "run_control", + "workflow_status", + "wake", + "workspace_context", + "integration_catalog", +] as const satisfies readonly SimControlKind[]; +export type BaselineSimControlKind = (typeof SIM_CONTROL_BASELINE_KINDS)[number]; +export type NegotiatedSimControlKind = Exclude; + +/** Error code of the 501 answer for a kind the receiving Sim did not declare. */ +export const SIM_CONTROL_UNSUPPORTED = "sim_control_unsupported"; + +/** `accepts` is free-form so a newer Sim's kinds never fail an older worker's poll. */ +export const SimChannelPoll = z.object({ + channelId: SimChannelId, + accepts: z.array(z.string().min(1).max(64)).max(64).optional(), +}); +/** The worker's typed batch; receivers parse `SimChannelBatchEnvelope` instead. */ export const SimChannelBatch = z.object({ requests: z.array(SimControlRequest).max(16) }); -export const SimChannelReply = SimChannelPoll.extend({ +/** Receivers parse each request alone, so one unknown kind never poisons its batch-mates. */ +export const SimChannelBatchEnvelope = z.object({ requests: z.array(z.unknown()).max(16) }); +/** Enough of a request to answer it when its operation does not parse. */ +export const SimControlRequestHeader = z.object({ + id: z.uuid(), + operation: z.object({ kind: z.string() }).optional().catch(undefined), +}); +export const SimChannelReply = z.object({ + channelId: SimChannelId, id: z.uuid(), result: SimControlResult, }); diff --git a/apps/sim/lib/mothership/memories/application/resolve-access.integration.ts b/apps/sim/lib/mothership/memories/application/resolve-access.integration.ts new file mode 100644 index 00000000000..72a6033d0a5 --- /dev/null +++ b/apps/sim/lib/mothership/memories/application/resolve-access.integration.ts @@ -0,0 +1,587 @@ +import { db } from '@sim/db' +import { + copilotChats, + member, + organization, + permissionGroup, + permissionGroupWorkspace, + permissions, + project, + user, + workspace, +} from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' +import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' +import { sleep } from '@sim/utils/helpers' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import { NextRequest } from 'next/server' +import { afterAll, beforeAll, beforeEach, expect, it, vi } from 'vitest' +import { + createTrustedCopilotPrincipal, + createTrustedOrganizationCopilotPrincipal, +} from '@/lib/mothership/auth/application-delegation' +import { MemoryNotesAccessResult } from '@/lib/mothership/generated/memory-notes-access' +import { + MEMORY_NOTES_ACCESS_AUDIENCE, + resolveMemoryNotesAccess, +} from '@/lib/mothership/memories/application/resolve-access' +import { executeSimControl } from '@/lib/mothership/transport/control' +import { lockProject, splitForkProject } from '@/lib/projects/membership' +import { POST } from '@/app/api/mothership/memories/access/route' + +vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) + +beforeEach(() => { + featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation(async (flag) => flag === 'projects') +}) + +const actorId = generateId() +const payerId = generateId() +const organizationId = generateId() +const projectId = generateId() +const workspaceId = generateId() +const siblingId = generateId() +const workspaceChatId = generateId() +const organizationChatId = generateId() + +function principal(organizationChat = false) { + const options = { audience: MEMORY_NOTES_ACCESS_AUDIENCE, ttlMs: 60_000 } + return organizationChat + ? createTrustedOrganizationCopilotPrincipal( + { userId: actorId, organizationId, chatId: organizationChatId, delegationId: generateId() }, + options + ) + : createTrustedCopilotPrincipal( + { userId: actorId, workspaceId, chatId: workspaceChatId, delegationId: generateId() }, + options + ) +} + +beforeAll(async () => { + await db.insert(user).values( + [actorId, payerId].map((id) => ({ + id, + name: 'Memory access fixture', + email: `${id}@memory.test`, + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + })) + ) + await db.insert(organization).values({ + id: organizationId, + name: 'Memory fixture', + slug: organizationId, + createdAt: new Date(), + }) + await db.insert(member).values([ + { id: generateId(), organizationId, userId: payerId, role: 'owner', createdAt: new Date() }, + { id: generateId(), organizationId, userId: actorId, role: 'member', createdAt: new Date() }, + ]) + await insertWorkspaceFixture( + db, + [workspaceId, siblingId].map((id) => ({ + id, + name: 'Environment', + ownerId: payerId, + billedAccountUserId: payerId, + organizationId, + workspaceMode: 'organization' as const, + projectId, + })) + ) + await db.insert(permissions).values({ + id: generateId(), + userId: actorId, + entityType: 'workspace', + entityId: workspaceId, + permissionType: 'read', + }) + await db.insert(copilotChats).values([ + { + id: workspaceChatId, + userId: actorId, + workspaceId, + type: 'mothership', + config: { conversationMode: 'agent' }, + }, + { + id: organizationChatId, + userId: actorId, + organizationId, + type: 'mothership', + config: { conversationMode: 'agent' }, + }, + ]) +}) + +afterAll(async () => { + await db + .delete(copilotChats) + .where(inArray(copilotChats.id, [workspaceChatId, organizationChatId])) + await deleteWorkspaceFixture(db, inArray(workspace.id, [workspaceId, siblingId])) + await db.delete(project).where(eq(project.id, projectId)) + await db.delete(organization).where(eq(organization.id, organizationId)) + await db.delete(user).where(inArray(user.id, [actorId, payerId])) + await db.$client.end() +}) + +it('denies every Project memory action on both transports when projects is disabled', async () => { + featureFlagsMockFns.mockIsFeatureEnabled.mockResolvedValue(false) + for (const organizationChat of [false, true]) { + const chatId = organizationChat ? organizationChatId : workspaceChatId + for (const action of ['read', 'private_mutation', 'shared_mutation'] as const) { + const input = { target: { scope: 'project' as const, projectId }, action } + const response = await POST( + new NextRequest('http://localhost/api/mothership/memories/access', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': 'isolated-integration-fixture-internal-secret', + 'x-mothership-user-id': actorId, + 'x-mothership-chat-id': chatId, + ...(organizationChat + ? { 'x-mothership-organization-id': organizationId } + : { 'x-mothership-workspace-id': workspaceId }), + }, + body: JSON.stringify(input), + }) + ) + expect(response.status).toBe(503) + const checkpoint = await executeSimControl({ + id: generateId(), + expiresAt: Date.now() + 60_000, + scope: { + userId: actorId, + chatId, + ...(organizationChat ? { organizationId } : { workspaceId }), + }, + operation: { kind: 'memory_notes_access', input }, + }) + expect(checkpoint.status).toBe(503) + expect(JSON.parse(checkpoint.body)).toEqual({ + error: 'Projects are not enabled on this deployment', + }) + } + } +}) + +it('keeps personal and workspace memories usable without Project context when projects is disabled', async () => { + featureFlagsMockFns.mockIsFeatureEnabled.mockResolvedValue(false) + for (const organizationChat of [false, true]) { + for (const action of ['read', 'private_mutation'] as const) { + const personal = await resolveMemoryNotesAccess.execute({ + principal: principal(organizationChat), + input: { target: { scope: 'personal' }, action }, + }) + expect(personal.target).toEqual({ + scope: 'personal', + permissions: { read: true, privateMutation: true, sharedMutation: false }, + }) + const result = await resolveMemoryNotesAccess.execute({ + principal: principal(organizationChat), + input: { target: { scope: 'workspace', workspaceId }, action }, + }) + expect(MemoryNotesAccessResult.parse(result).target).toEqual({ + scope: 'workspace', + id: workspaceId, + name: 'Environment', + permissions: { read: true, privateMutation: true, sharedMutation: false }, + currentProject: null, + }) + } + await expect( + resolveMemoryNotesAccess.execute({ + principal: principal(organizationChat), + input: { target: { scope: 'workspace', workspaceId }, action: 'shared_mutation' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + } + await db + .update(permissions) + .set({ permissionType: 'write' }) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId))) + try { + for (const organizationChat of [false, true]) { + const result = await resolveMemoryNotesAccess.execute({ + principal: principal(organizationChat), + input: { target: { scope: 'workspace', workspaceId }, action: 'shared_mutation' }, + }) + expect(result.target).toMatchObject({ + currentProject: null, + permissions: { sharedMutation: true }, + }) + } + } finally { + await db + .update(permissions) + .set({ permissionType: 'read' }) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId))) + } +}) + +it('lets a read-only end user mutate private notes through either chat and keeps Project shared writes denied', async () => { + for (const organizationChat of [false, true]) { + const result = await resolveMemoryNotesAccess.execute({ + principal: principal(organizationChat), + input: { target: { scope: 'workspace', workspaceId }, action: 'private_mutation' }, + }) + expect(result.actor.userId).toBe(actorId) + expect(result.target).toMatchObject({ + scope: 'workspace', + id: workspaceId, + permissions: { privateMutation: true, sharedMutation: false }, + currentProject: { id: projectId, permissions: { read: true, sharedMutation: false } }, + }) + await expect( + resolveMemoryNotesAccess.execute({ + principal: principal(organizationChat), + input: { target: { scope: 'project', projectId }, action: 'shared_mutation' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + } +}) + +it('resolves overlapping workspace checks that share one Project without deadlocking', async () => { + const results = await Promise.all( + Array.from({ length: 6 }, (_, index) => + resolveMemoryNotesAccess.execute({ + principal: principal(index % 2 === 1), + input: { target: { scope: 'workspace', workspaceId }, action: 'read' }, + }) + ) + ) + for (const result of results) + expect(result.target).toMatchObject({ scope: 'workspace', currentProject: { id: projectId } }) +}) + +it('answers while a writer holds the account row and the Project exclusively', async () => { + let release = () => {} + const released = new Promise((resolve) => { + release = resolve + }) + let held = () => {} + const holding = new Promise((resolve) => { + held = resolve + }) + const writer = db.transaction(async (tx) => { + await tx.select({ id: user.id }).from(user).where(eq(user.id, actorId)).for('update') + await lockProject(tx, projectId) + held() + await released + }) + await holding + try { + for (const organizationChat of [false, true]) { + const result = await Promise.race([ + resolveMemoryNotesAccess.execute({ + principal: principal(organizationChat), + input: { target: { scope: 'workspace', workspaceId }, action: 'read' }, + }), + sleep(2_000).then(() => { + throw new Error('Memory access waited on a writer') + }), + ]) + expect(result.target).toMatchObject({ currentProject: { id: projectId } }) + } + } finally { + release() + await writer + } +}) + +it('does not turn write in one environment into shared Project write', async () => { + await db + .update(permissions) + .set({ permissionType: 'write' }) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId))) + try { + const result = await resolveMemoryNotesAccess.execute({ + principal: principal(), + input: { target: { scope: 'workspace', workspaceId }, action: 'shared_mutation' }, + }) + expect(result.target).toMatchObject({ + permissions: { sharedMutation: true }, + currentProject: { permissions: { sharedMutation: false } }, + }) + await db.insert(permissions).values({ + id: generateId(), + userId: actorId, + entityType: 'workspace', + entityId: siblingId, + permissionType: 'write', + }) + const shared = await resolveMemoryNotesAccess.execute({ + principal: principal(true), + input: { target: { scope: 'project', projectId }, action: 'shared_mutation' }, + }) + expect(shared.target.permissions.sharedMutation).toBe(true) + } finally { + await db + .delete(permissions) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, siblingId))) + await db + .update(permissions) + .set({ permissionType: 'read' }) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId))) + } +}) + +it('rejects wrong actor, audience, expired delegation and another workspace target', async () => { + const input = { target: { scope: 'workspace' as const, workspaceId }, action: 'read' as const } + for (const altered of [ + { ...principal(), subjectUserId: payerId }, + { ...principal(), audience: 'sim:workspaces' }, + { ...principal(), expiresAt: new Date(0) }, + ]) + await expect( + resolveMemoryNotesAccess.execute({ principal: altered, input }) + ).rejects.toBeInstanceOf(Error) + await expect( + resolveMemoryNotesAccess.execute({ + principal: principal(), + input: { ...input, target: { scope: 'workspace', workspaceId: siblingId } }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) +}) + +it('denies a lost scoped grant while personal-only organization context remains usable', async () => { + await db + .delete(permissions) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId))) + try { + await expect( + resolveMemoryNotesAccess.execute({ + principal: principal(true), + input: { target: { scope: 'project', projectId }, action: 'private_mutation' }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + const result = await resolveMemoryNotesAccess.execute({ + principal: principal(true), + input: { target: { scope: 'personal' }, action: 'private_mutation' }, + }) + expect(result.actor.userId).toBe(actorId) + expect(result.target.scope).toBe('personal') + } finally { + await db.insert(permissions).values({ + id: generateId(), + userId: actorId, + entityType: 'workspace', + entityId: workspaceId, + permissionType: 'read', + }) + } +}) + +it('applies the Files capability to shared Project writes without blocking private notes', async () => { + const groupId = generateId() + await db + .update(permissions) + .set({ permissionType: 'write' }) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId))) + await db.insert(permissions).values({ + id: generateId(), + userId: actorId, + entityType: 'workspace', + entityId: siblingId, + permissionType: 'write', + }) + await db.insert(permissionGroup).values({ + id: groupId, + organizationId, + createdBy: payerId, + name: 'Files denied', + config: { hideFilesTab: true }, + membershipMode: 'inherit', + }) + await db.insert(permissionGroupWorkspace).values({ + id: generateId(), + permissionGroupId: groupId, + workspaceId: siblingId, + organizationId, + }) + try { + const result = await resolveMemoryNotesAccess.execute({ + principal: principal(true), + input: { target: { scope: 'project', projectId }, action: 'private_mutation' }, + }) + expect(result.target.permissions).toEqual({ + read: true, + privateMutation: true, + sharedMutation: false, + }) + await expect( + resolveMemoryNotesAccess.execute({ + principal: principal(true), + input: { target: { scope: 'project', projectId }, action: 'shared_mutation' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + } finally { + await db.delete(permissionGroup).where(eq(permissionGroup.id, groupId)) + await db + .delete(permissions) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, siblingId))) + await db + .update(permissions) + .set({ permissionType: 'read' }) + .where(and(eq(permissions.userId, actorId), eq(permissions.entityId, workspaceId))) + } +}) + +it('rejects cross-organization authority and organization non-agent mode', async () => { + const otherId = generateId() + await db + .insert(organization) + .values({ id: otherId, name: 'Other organization', slug: otherId, createdAt: new Date() }) + await db.update(member).set({ organizationId: otherId }).where(eq(member.userId, actorId)) + await db + .update(copilotChats) + .set({ organizationId: otherId }) + .where(eq(copilotChats.id, organizationChatId)) + try { + const otherPrincipal = { ...principal(true), organizationId: otherId } + await expect( + resolveMemoryNotesAccess.execute({ + principal: otherPrincipal, + input: { target: { scope: 'project', projectId }, action: 'read' }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + await expect( + resolveMemoryNotesAccess.execute({ + principal: otherPrincipal, + input: { target: { scope: 'workspace', workspaceId }, action: 'read' }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + } finally { + await db + .update(copilotChats) + .set({ organizationId }) + .where(eq(copilotChats.id, organizationChatId)) + await db.update(member).set({ organizationId }).where(eq(member.userId, actorId)) + await db.delete(organization).where(eq(organization.id, otherId)) + } + await db + .update(copilotChats) + .set({ config: { conversationMode: 'assistant' } }) + .where(eq(copilotChats.id, organizationChatId)) + try { + await expect( + resolveMemoryNotesAccess.execute({ + principal: principal(true), + input: { target: { scope: 'project', projectId }, action: 'read' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + } finally { + await db + .update(copilotChats) + .set({ config: { conversationMode: 'agent' } }) + .where(eq(copilotChats.id, organizationChatId)) + } +}) + +it('serves the typed route using authenticated headers and rejects body identity overrides', async () => { + const input = { target: { scope: 'workspace', workspaceId }, action: 'private_mutation' } + const request = (body: unknown, authenticated = true) => + new NextRequest('http://localhost/api/mothership/memories/access', { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...(authenticated ? { 'x-api-key': 'isolated-integration-fixture-internal-secret' } : {}), + 'x-mothership-user-id': actorId, + 'x-mothership-workspace-id': workspaceId, + 'x-mothership-chat-id': workspaceChatId, + }, + body: JSON.stringify(body), + }) + expect((await POST(request(input, false))).status).toBe(401) + expect((await POST(request({ ...input, userId: payerId }))).status).toBe(400) + expect( + (await POST(request({ ...input, target: { scope: 'workspace', workspaceId, projectId } }))) + .status + ).toBe(400) + for (const projectsEnabled of [true, false, true]) { + featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation( + async (flag) => flag === 'projects' && projectsEnabled + ) + const response = await POST(request(input)) + expect(response.status).toBe(200) + expect(MemoryNotesAccessResult.parse(await response.json())).toMatchObject({ + success: true, + version: 1, + actor: { userId: actorId, chatId: workspaceChatId }, + target: { currentProject: projectsEnabled ? { id: projectId } : null }, + }) + } +}) + +it('resolves detach to the new Project and rejects the former Project from the workspace chat', async () => { + const detachedProject = await db.transaction(async (tx) => { + const id = await splitForkProject(tx, workspaceId) + await tx + .update(workspace) + .set({ forkedFromWorkspaceId: null }) + .where(eq(workspace.id, workspaceId)) + return id + }) + try { + const result = await resolveMemoryNotesAccess.execute({ + principal: principal(), + input: { target: { scope: 'workspace', workspaceId }, action: 'read' }, + }) + expect(result.target).toMatchObject({ currentProject: { id: detachedProject } }) + await expect( + resolveMemoryNotesAccess.execute({ + principal: principal(), + input: { target: { scope: 'project', projectId }, action: 'read' }, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + } finally { + await db.transaction(async (tx) => { + await tx.update(workspace).set({ projectId }).where(eq(workspace.id, workspaceId)) + await tx.delete(project).where(eq(project.id, detachedProject)) + }) + } +}) + +it('uses identical current authority over checkpoint transport for workspace and organization chats', async () => { + for (const [organizationChat, projectsEnabled] of [ + [false, true], + [false, false], + [true, true], + [true, false], + ]) { + featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation( + async (flag) => flag === 'projects' && projectsEnabled + ) + const scope = { + userId: actorId, + chatId: organizationChat ? organizationChatId : workspaceChatId, + ...(organizationChat ? { organizationId } : { workspaceId }), + } + const result = await executeSimControl({ + id: generateId(), + expiresAt: Date.now() + 60_000, + scope, + operation: { + kind: 'memory_notes_access', + input: { target: { scope: 'workspace', workspaceId }, action: 'private_mutation' }, + }, + }) + expect(result.status).toBe(200) + expect(MemoryNotesAccessResult.parse(JSON.parse(result.body))).toMatchObject({ + actor: { userId: actorId, chatId: scope.chatId }, + target: { id: workspaceId, currentProject: projectsEnabled ? { id: projectId } : null }, + }) + const denied = await executeSimControl({ + id: generateId(), + expiresAt: Date.now() + 60_000, + scope, + operation: { + kind: 'memory_notes_access', + input: { target: { scope: 'project', projectId }, action: 'shared_mutation' }, + }, + }) + expect(denied.status).toBe(projectsEnabled ? 403 : 503) + } +}) diff --git a/apps/sim/lib/mothership/memories/application/resolve-access.ts b/apps/sim/lib/mothership/memories/application/resolve-access.ts new file mode 100644 index 00000000000..a52ba63bc29 --- /dev/null +++ b/apps/sim/lib/mothership/memories/application/resolve-access.ts @@ -0,0 +1,268 @@ +import type { OrganizationDelegatedPrincipal, SubjectDelegatedPrincipal } from '@sim/auth/principal' +import { db } from '@sim/db' +import { copilotChats, project, workspace } from '@sim/db/schema' +import { permissionSatisfies } from '@sim/platform-authz/workspace' +import { and, eq, isNull } from 'drizzle-orm' +import { defineWorkspaceOperation } from '@/lib/core/application' +import { authorizeOrganizationOperation } from '@/lib/core/application/organization-authorization' +import { defineOrganizationOperation } from '@/lib/core/application/organization-operation' +import { requireCurrentHumanRole } from '@/lib/core/application/workspace-authorization' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { DbTransaction } from '@/lib/db/types' +import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case' +import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context' +import { conversationModeSelection } from '@/lib/mothership/chat/intent' +import type { + MemoryNotesAccessInput, + MemoryNotesAccessResult, +} from '@/lib/mothership/generated/memory-notes-access' +import { assertWorkspaceCapability } from '@/lib/permission-groups/capability-assertions' +import { PermissionGroupCapabilityError } from '@/lib/permission-groups/capability-error' +import { loadProjectAccess } from '@/lib/projects/application/authorization' +import { + requireCurrentCopilotProjectInvocation, + requireProjectFileOwnerCapabilities, + requireProjectFileOwnerRole, +} from '@/lib/projects/files/application/authorization' +import { isProjectsEnabled, requireProjectApiEnabled } from '@/lib/projects/rollout.server' +import { requireCurrentFileSubject } from '@/lib/workspace-files/application/subject' + +export const MEMORY_NOTES_ACCESS_AUDIENCE = 'sim:memory-access' +export const resolveMemoryNotesAccessOperation = defineWorkspaceOperation({ + id: 'mothership.memories.resolve_access', + contentDelivery: 'none', + minimumRole: 'read', + workspaceApiKey: 'deny', + capability: 'copilot.use', + principalKinds: ['delegated'], + delegatedServices: ['copilot'], +}) +const organizationOperation = defineOrganizationOperation({ + id: resolveMemoryNotesAccessOperation.id, + contentDelivery: resolveMemoryNotesAccessOperation.contentDelivery, + minimumRole: 'member', + capability: 'copilot.use', + principalKinds: ['organization_delegated'], + delegatedServices: ['copilot'], + delegationAudience: MEMORY_NOTES_ACCESS_AUDIENCE, +}) +type MemoryPrincipal = SubjectDelegatedPrincipal | OrganizationDelegatedPrincipal +/** The worker acts on the answer after commit, so a consistent unlocked snapshot is enough. */ +const READ_SNAPSHOT = { isolationLevel: 'repeatable read', accessMode: 'read only' } as const + +async function workspaceProject(tx: DbTransaction, workspaceId: string) { + const [row] = await tx + .select({ id: project.id, organizationId: project.organizationId }) + .from(workspace) + .innerJoin(project, eq(project.id, workspace.projectId)) + .where(eq(workspace.id, workspaceId)) + .limit(1) + return row ?? null +} + +async function workspaceAccess(tx: DbTransaction, userId: string, workspaceId: string) { + const [target] = await tx + .select({ + workspaceId: workspace.id, + name: workspace.name, + workspaceOrganizationId: workspace.organizationId, + allowPersonalApiKeys: workspace.allowPersonalApiKeys, + }) + .from(workspace) + .where(and(eq(workspace.id, workspaceId), isNull(workspace.archivedAt))) + .limit(1) + if (!target) throw new OrchestrationError('not_found', 'Workspace not found') + const permission = await requireCurrentHumanRole(userId, target, 'read', { executor: tx }) + await assertWorkspaceCapability( + userId, + workspaceId, + 'copilot.use', + target.workspaceOrganizationId, + tx + ) + return { target, sharedMutation: permissionSatisfies(permission, 'write') } +} + +async function projectAccess( + tx: DbTransaction, + principal: MemoryPrincipal, + chatId: string, + projectId: string, + scope: { organizationId: string | null; workspaceId?: string } +): Promise> { + const access = await loadProjectAccess( + tx, + principal.subjectUserId, + { + projectId, + ...(scope.organizationId ? { organizationId: scope.organizationId } : {}), + ...(scope.workspaceId ? { workspaceId: scope.workspaceId } : {}), + }, + 'snapshot' + ) + if (scope.organizationId !== access.record.organizationId) + throw new OrchestrationError('not_found', 'Project not found in this conversation') + await requireCurrentCopilotProjectInvocation( + tx, + { + subjectUserId: principal.subjectUserId, + invocation: { kind: 'chat', chatId }, + }, + access, + scope, + 'snapshot' + ) + const owner = await requireProjectFileOwnerRole(tx, principal, access, 'read', 'snapshot') + let sharedMutation = owner.canWrite + if (sharedMutation) { + try { + await requireProjectFileOwnerCapabilities(tx, principal, access, 'files.use') + } catch (error) { + if (!(error instanceof PermissionGroupCapabilityError)) throw error + sharedMutation = false + } + } + return { + scope: 'project', + id: access.record.id, + name: access.record.name, + permissions: { read: true, privateMutation: true, sharedMutation }, + } +} + +/** Current Sim authority only; Mothership must re-resolve before each read or mutation. */ +const resolveAccess = defineAuthorizedChatUseCase({ + operation: resolveMemoryNotesAccessOperation, + organizationOperation, + async resolveContext({ + principal, + }: { + principal: MemoryPrincipal + input: MemoryNotesAccessInput + }) { + const chatId = + principal.resourceScope && 'chatId' in principal.resourceScope + ? principal.resourceScope.chatId + : undefined + const now = Date.now() + if ( + principal.serviceId !== 'copilot' || + principal.audience !== MEMORY_NOTES_ACCESS_AUDIENCE || + !chatId || + !Number.isFinite(principal.issuedAt.getTime()) || + !Number.isFinite(principal.expiresAt.getTime()) || + principal.issuedAt.getTime() > now || + principal.expiresAt.getTime() <= now || + principal.expiresAt.getTime() - principal.issuedAt.getTime() > 60_000 + ) + throw new OrchestrationError('forbidden', 'Memory delegation is no longer valid') + return resolveOwnedChatContext(principal, chatId) + }, + authorizationOptions: { + delegation: { + audience: MEMORY_NOTES_ACCESS_AUDIENCE, + isWithinScope: (principal, context) => principal.resourceScope?.chatId === context.chatId, + }, + }, + async execute({ principal, context, input }): Promise { + const projectsEnabled = input.target.scope !== 'personal' && (await isProjectsEnabled()) + return db.transaction(async (tx) => { + await requireCurrentFileSubject(tx, principal, 'snapshot') + const [chat] = await tx + .select({ + userId: copilotChats.userId, + workspaceId: copilotChats.workspaceId, + organizationId: copilotChats.organizationId, + type: copilotChats.type, + mode: conversationModeSelection, + }) + .from(copilotChats) + .where(and(eq(copilotChats.id, context.chatId), isNull(copilotChats.deletedAt))) + .limit(1) + if ( + !chat || + chat.userId !== principal.subjectUserId || + chat.workspaceId !== (context.workspaceId ?? null) || + chat.organizationId !== (context.organizationId ?? null) || + (chat.organizationId && chat.type !== 'mothership') + ) + throw new OrchestrationError('not_found', 'Chat not found') + if (chat.organizationId) { + if (chat.mode !== 'agent' && chat.mode !== 'plan') + throw new OrchestrationError( + 'forbidden', + 'Memory access requires organization agent mode' + ) + await authorizeOrganizationOperation( + principal, + organizationOperation, + { organizationId: chat.organizationId }, + { executor: tx } + ) + } + const origin = + chat.workspaceId && input.target.scope !== 'personal' + ? await workspaceProject(tx, chat.workspaceId) + : null + const originAccess = chat.workspaceId + ? await workspaceAccess(tx, principal.subjectUserId, chat.workspaceId) + : null + const scope = { + organizationId: chat.organizationId ?? origin?.organizationId ?? null, + ...(chat.workspaceId ? { workspaceId: chat.workspaceId } : {}), + } + let target: MemoryNotesAccessResult['target'] + if (input.target.scope === 'personal') { + target = { + scope: 'personal', + permissions: { read: true, privateMutation: true, sharedMutation: false }, + } + } else if (input.target.scope === 'project') { + if (!projectsEnabled) await requireProjectApiEnabled() + if (chat.workspaceId && origin?.id !== input.target.projectId) + throw new OrchestrationError('not_found', 'Project not found in this conversation') + target = await projectAccess(tx, principal, context.chatId, input.target.projectId, scope) + } else { + if (chat.workspaceId && chat.workspaceId !== input.target.workspaceId) + throw new OrchestrationError('not_found', 'Workspace not found in this conversation') + const membership = chat.workspaceId + ? origin + : await workspaceProject(tx, input.target.workspaceId) + if (!membership || membership.organizationId !== scope.organizationId) + throw new OrchestrationError('not_found', 'Workspace not found in this conversation') + const access = + originAccess ?? + (await workspaceAccess(tx, principal.subjectUserId, input.target.workspaceId)) + if (access.target.workspaceOrganizationId !== scope.organizationId) + throw new OrchestrationError('not_found', 'Workspace not found in this conversation') + const currentProject = projectsEnabled + ? await projectAccess(tx, principal, context.chatId, membership.id, scope) + : null + target = { + scope: 'workspace', + id: access.target.workspaceId, + name: access.target.name, + permissions: { read: true, privateMutation: true, sharedMutation: access.sharedMutation }, + currentProject, + } + } + if (input.action === 'shared_mutation' && !target.permissions.sharedMutation) + throw new OrchestrationError('forbidden', 'Shared memory mutation is not permitted') + return { + success: true, + version: 1, + actor: { userId: principal.subjectUserId, chatId: context.chatId }, + target, + } + }, READ_SNAPSHOT) + }, +}) + +/** Authorization-only callers resolve the same requested target and permission as execution. */ +export const resolveMemoryNotesAccess = { + operation: resolveMemoryNotesAccessOperation, + execute: resolveAccess.execute, + async authorize(args: Parameters[0]): Promise { + await resolveAccess.execute(args) + }, +} diff --git a/apps/sim/lib/mothership/memories/cleanup-event.ts b/apps/sim/lib/mothership/memories/cleanup-event.ts new file mode 100644 index 00000000000..2ec04541059 --- /dev/null +++ b/apps/sim/lib/mothership/memories/cleanup-event.ts @@ -0,0 +1 @@ +export const MEMORY_CLEANUP_EVENT = 'mothership.memory.cleanup' diff --git a/apps/sim/lib/mothership/memories/cleanup-outbox.test.ts b/apps/sim/lib/mothership/memories/cleanup-outbox.test.ts new file mode 100644 index 00000000000..25c1da8673b --- /dev/null +++ b/apps/sim/lib/mothership/memories/cleanup-outbox.test.ts @@ -0,0 +1,40 @@ +/** + * @vitest-environment node + */ +import { resetEnvMock, setEnv } from '@sim/testing/mocks/env.mock' +import { mothershipAgentUrlMock } from '@sim/testing/mocks/mothership-agent-url.mock' +import { mothershipGoFetchMock } from '@sim/testing/mocks/mothership-go-fetch.mock' +import { outboxServiceMock, outboxServiceMockFns } from '@sim/testing/mocks/outbox-service.mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/outbox/service', () => outboxServiceMock) +vi.mock('@/lib/mothership/constants', () => ({ SIM_AGENT_API_URL: 'https://agent.example.test' })) +vi.mock('@/lib/mothership/request/go/fetch', () => mothershipGoFetchMock) +vi.mock('@/lib/mothership/request/headers', () => ({ mothershipRequestHeaders: () => ({}) })) +vi.mock('@/lib/mothership/server/agent-url', () => mothershipAgentUrlMock) + +import { enqueueMemoryCleanup } from '@/lib/mothership/memories/cleanup-outbox' + +const { mockEnqueueOutboxEvents } = outboxServiceMockFns +const executor = { insert: vi.fn() } +const targets = [ + { kind: 'private_owner' as const, userId: 'user-1' }, + { kind: 'workspace' as const, workspaceId: '00000000-0000-4000-8000-000000000001' }, +] + +describe('enqueueMemoryCleanup', () => { + beforeEach(() => { + resetEnvMock() + setEnv({ + COPILOT_API_KEY: undefined, + COPILOT_DEV_URL: undefined, + COPILOT_STAGING_URL: undefined, + COPILOT_PROD_URL: undefined, + }) + }) + + it('queues nothing when this Sim has no worker credential', async () => { + await enqueueMemoryCleanup(executor, targets) + expect(mockEnqueueOutboxEvents).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/mothership/memories/cleanup-outbox.ts b/apps/sim/lib/mothership/memories/cleanup-outbox.ts new file mode 100644 index 00000000000..cd7fa5e3037 --- /dev/null +++ b/apps/sim/lib/mothership/memories/cleanup-outbox.ts @@ -0,0 +1,131 @@ +import type { db } from '@sim/db' +import { createLogger } from '@sim/logger' +import { chunkArray } from '@sim/utils/helpers' +import { omit } from '@sim/utils/object' +import { z } from 'zod' +import { env } from '@/lib/core/config/env' +import { + deferOutboxHandler, + enqueueOutboxEvents, + MAX_BULK_ENQUEUE_EVENTS, + type OutboxHandlerRegistry, +} from '@/lib/core/outbox/service' +import { SIM_AGENT_API_URL } from '@/lib/mothership/constants' +import { + MemoryCleanupInput, + MemoryCleanupResult, +} from '@/lib/mothership/generated/memory-notes-access' +import { MEMORY_CLEANUP_EVENT } from '@/lib/mothership/memories/cleanup-event' +import { fetchGo } from '@/lib/mothership/request/go/fetch' +import { mothershipRequestHeaders } from '@/lib/mothership/request/headers' +import { + getMothershipSourceEnvHeaders, + MOTHERSHIP_SOURCE_ENV_HEADER, +} from '@/lib/mothership/server/agent-url' + +const logger = createLogger('MemoryCleanup') + +/** Waits for a worker or configuration that is not there yet without spending the retry budget. */ +const UNAVAILABLE_RETRY_MS = 60 * 60 * 1000 +/** + * Statuses meaning the worker does not serve cleanup yet, not that this request failed: a worker + * that predates the route answers 404 to the hosted key. + */ +const ENDPOINT_MISSING_STATUSES = new Set([404, 405, 501]) +/** + * A worker that predates the route also answers this to a self-hosted customer key, but so does a + * worker that revoked the key for good: it waits like a missing route but spends an attempt, so a + * revoked key dead-letters after the default attempts (about ten hours), past a worker rollout. + */ +const UNAUTHORIZED_STATUS = 401 + +/** Non-strict so rows enqueued with the retired `credentialFingerprint` field still parse. */ +const cleanupPayloadSchema = z.object({ + request: MemoryCleanupInput, + baseUrl: z.url(), + sourceHeaders: z.object({ + [MOTHERSHIP_SOURCE_ENV_HEADER]: z.enum(['dev', 'staging', 'prod']).optional(), + }), +}) + +function destinations(): string[] { + return [ + ...new Set( + [SIM_AGENT_API_URL, env.COPILOT_DEV_URL, env.COPILOT_STAGING_URL, env.COPILOT_PROD_URL] + .filter((value): value is string => Boolean(value)) + .filter((value) => value.startsWith('https://') || value.startsWith('http://')) + .map((value) => value.replace(/\/+$/, '')) + ), + ] +} + +/** + * Captures configured routes before deletion; notes can outlive a user's environment selection. + * Without `COPILOT_API_KEY` this Sim never reached a worker, so it holds no notes to erase and + * queues nothing rather than an event no delivery could authenticate. + */ +export async function enqueueMemoryCleanup( + executor: Pick, + targets: MemoryCleanupInput['target'][] +): Promise { + if (!env.COPILOT_API_KEY) return + const sourceHeaders = getMothershipSourceEnvHeaders() + const payloads = destinations().flatMap((baseUrl) => + targets.map((target) => + cleanupPayloadSchema.parse({ request: { version: 1, target }, baseUrl, sourceHeaders }) + ) + ) + for (const batch of chunkArray(payloads, MAX_BULK_ENQUEUE_EVENTS)) { + await enqueueOutboxEvents(executor, MEMORY_CLEANUP_EVENT, batch) + } +} + +function waitForWorker( + reason: string, + eventId: string, + { consumeAttempt = false, ...detail }: { baseUrl?: string; consumeAttempt?: boolean } = {} +) { + logger.warn(`Memory cleanup waiting: ${reason}`, { eventId, ...detail }) + return deferOutboxHandler(reason, UNAVAILABLE_RETRY_MS, consumeAttempt) +} + +/** + * Sends the processor's current key only to a destination the processor itself configures, and + * replays the enqueuing deployment's source env so the worker erases that deployment's notes. + * A missing key, destination, or worker endpoint waits without consuming attempts: erasure must + * not dead-letter while configuration or the worker rollout catches up. A rejected key waits too, + * but within the retry budget. + */ +export const memoryCleanupOutboxHandlers = { + [MEMORY_CLEANUP_EVENT]: async (rawPayload, context) => { + const payload = cleanupPayloadSchema.parse(rawPayload) + if (!env.COPILOT_API_KEY) + return waitForWorker('COPILOT_API_KEY is not configured', context.eventId) + if (!destinations().includes(payload.baseUrl)) + return waitForWorker('destination is not configured', context.eventId, { + baseUrl: payload.baseUrl, + }) + const sourceEnv = payload.sourceHeaders[MOTHERSHIP_SOURCE_ENV_HEADER] + const response = await fetchGo(`${payload.baseUrl}/api/memories/cleanup`, { + method: 'POST', + headers: { + ...omit(mothershipRequestHeaders(), [MOTHERSHIP_SOURCE_ENV_HEADER]), + ...(sourceEnv ? { [MOTHERSHIP_SOURCE_ENV_HEADER]: sourceEnv } : {}), + }, + body: JSON.stringify(payload.request), + signal: AbortSignal.any([context.signal, AbortSignal.timeout(10_000)]), + redirect: 'error', + }) + if (!response.ok) { + await response.body?.cancel().catch(() => {}) + if (ENDPOINT_MISSING_STATUSES.has(response.status)) + return waitForWorker(`endpoint unavailable (${response.status})`, context.eventId) + if (response.status === UNAUTHORIZED_STATUS) + return waitForWorker('worker rejected the key (401)', context.eventId, { + consumeAttempt: true, + }) + throw new Error(`Memory cleanup failed (${response.status})`) + } + MemoryCleanupResult.parse(await response.json()) + }, +} satisfies OutboxHandlerRegistry diff --git a/apps/sim/lib/mothership/memories/cleanup.integration.ts b/apps/sim/lib/mothership/memories/cleanup.integration.ts new file mode 100644 index 00000000000..a1ee6900ab0 --- /dev/null +++ b/apps/sim/lib/mothership/memories/cleanup.integration.ts @@ -0,0 +1,178 @@ +import { createServer } from 'node:http' +import { db } from '@sim/db' +import { outboxEvent, permissions, project, user, workspace } from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' +import { generateId } from '@sim/utils/id' +import { toRecord } from '@sim/utils/object' +import { and, eq, inArray, sql } from 'drizzle-orm' +import { afterAll, beforeAll, expect, it } from 'vitest' + +const userId = generateId() +const workspaceId = generateId() +const projectId = generateId() +const received: { body: unknown; key: string | string[] | undefined }[] = [] +let status = 503 +const server = createServer(async (req, res) => { + let body = '' + for await (const chunk of req) body += chunk + received.push({ body: JSON.parse(body), key: req.headers['x-api-key'] }) + res.writeHead(status, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ success: status === 200 })) +}) +let runtime: { + deletion: typeof import('@/lib/users/account-deletion') + cleanup: typeof import('@/lib/mothership/memories/cleanup-outbox') + outbox: typeof import('@/lib/core/outbox/service') + event: typeof import('@/lib/mothership/memories/cleanup-event') +} +let eventIds: string[] = [] + +beforeAll(async () => { + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + const address = server.address() + if (!address || typeof address === 'string') throw new Error('Missing fixture address') + process.env.SIM_AGENT_API_URL = `http://127.0.0.1:${address.port}` + process.env.COPILOT_API_KEY = 'memory-cleanup-fixture-key' + const [deletion, cleanup, outbox, event] = await Promise.all([ + import('@/lib/users/account-deletion'), + import('@/lib/mothership/memories/cleanup-outbox'), + import('@/lib/core/outbox/service'), + import('@/lib/mothership/memories/cleanup-event'), + ]) + runtime = { deletion, cleanup, outbox, event } + await db.insert(user).values({ + id: userId, + name: 'Cleanup fixture', + email: `${userId}@memory.test`, + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + }) + await insertWorkspaceFixture(db, { + id: workspaceId, + projectId, + name: 'Cleanup fixture', + ownerId: userId, + billedAccountUserId: userId, + }) + await db.insert(permissions).values({ + id: generateId(), + userId, + entityType: 'workspace', + entityId: workspaceId, + permissionType: 'admin', + }) +}, 60_000) + +afterAll(async () => { + if (eventIds.length) await db.delete(outboxEvent).where(inArray(outboxEvent.id, eventIds)) + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) + await db.delete(project).where(eq(project.id, projectId)) + await db.delete(user).where(eq(user.id, userId)) + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ) +}) + +it('rolls cleanup back with deletion and retries committed account/target erasure after the account is gone', async () => { + await expect( + db.transaction(async (tx) => { + await runtime.cleanup.enqueueMemoryCleanup(tx, [{ kind: 'private_owner', userId }]) + throw new Error('rollback fixture') + }) + ).rejects.toThrow('rollback fixture') + const pending = () => + db + .select() + .from(outboxEvent) + .where( + and( + eq(outboxEvent.eventType, runtime.event.MEMORY_CLEANUP_EVENT), + sql`(${outboxEvent.payload}->'request'->'target'->>'userId' = ${userId} + OR ${outboxEvent.payload}->'request'->'target'->>'workspaceId' = ${workspaceId} + OR ${outboxEvent.payload}->'request'->'target'->>'projectId' = ${projectId})` + ) + ) + expect(await pending()).toEqual([]) + + await runtime.deletion.deleteUserAccount(userId) + expect(await db.select({ id: user.id }).from(user).where(eq(user.id, userId))).toEqual([]) + expect( + await db.select({ id: workspace.id }).from(workspace).where(eq(workspace.id, workspaceId)) + ).toEqual([]) + expect( + await db.select({ id: project.id }).from(project).where(eq(project.id, projectId)) + ).toEqual([]) + const events = await pending() + eventIds = events.map((event) => event.id) + expect(events).toHaveLength(3) + expect(JSON.stringify(events)).not.toContain('memory-cleanup-fixture-key') + expect(received).toHaveLength(0) + for (const event of events) { + await runtime.outbox.processOutboxEventById( + event.id, + runtime.cleanup.memoryCleanupOutboxHandlers + ) + } + expect( + (await pending()).every((event) => event.status === 'pending' && event.attempts === 1) + ).toBe(true) + status = 200 + await db + .update(outboxEvent) + .set({ availableAt: new Date(0) }) + .where(inArray(outboxEvent.id, eventIds)) + for (const event of events) { + await runtime.outbox.processOutboxEventById( + event.id, + runtime.cleanup.memoryCleanupOutboxHandlers + ) + } + expect((await pending()).every((event) => event.status === 'completed')).toBe(true) + expect(received.slice(3).map((request) => request.body)).toEqual( + expect.arrayContaining([ + { version: 1, target: { kind: 'private_owner', userId } }, + { version: 1, target: { kind: 'workspace', workspaceId } }, + { version: 1, target: { kind: 'project', projectId } }, + ]) + ) + expect(received.every((request) => request.key === 'memory-cleanup-fixture-key')).toBe(true) + + const first = events[0] + const replay = async (payload: unknown = first.payload, attempts = 9) => { + await db + .update(outboxEvent) + .set({ status: 'pending', availableAt: new Date(0), attempts, payload }) + .where(eq(outboxEvent.id, first.id)) + await runtime.outbox.processOutboxEventById( + first.id, + runtime.cleanup.memoryCleanupOutboxHandlers + ) + const [row] = await db.select().from(outboxEvent).where(eq(outboxEvent.id, first.id)) + return row + } + const waitsAnHour = (row: typeof outboxEvent.$inferSelect) => { + expect(row).toMatchObject({ status: 'pending', attempts: 9 }) + expect(row.availableAt.getTime()).toBeGreaterThan(Date.now() + 50 * 60_000) + } + + status = 404 + const before = received.length + waitsAnHour(await replay()) + expect(received).toHaveLength(before + 1) + + status = 401 + const rejected = await replay(first.payload, 5) + expect(rejected).toMatchObject({ status: 'pending', attempts: 6 }) + expect(rejected.availableAt.getTime()).toBeGreaterThan(Date.now() + 50 * 60_000) + expect(await replay()).toMatchObject({ status: 'dead_letter', attempts: 10 }) + expect(received).toHaveLength(before + 3) + + status = 200 + waitsAnHour(await replay({ ...toRecord(first.payload), baseUrl: 'https://unconfigured.test' })) + expect(received).toHaveLength(before + 3) + + const legacy = await replay({ ...toRecord(first.payload), credentialFingerprint: 'retired' }) + expect(legacy.status).toBe('completed') + expect(received).toHaveLength(before + 4) +}, 30_000) diff --git a/apps/sim/lib/mothership/request/application/abandoned-runs.integration.ts b/apps/sim/lib/mothership/request/application/abandoned-runs.integration.ts index e759910a523..2a954363179 100644 --- a/apps/sim/lib/mothership/request/application/abandoned-runs.integration.ts +++ b/apps/sim/lib/mothership/request/application/abandoned-runs.integration.ts @@ -100,6 +100,7 @@ import { and, eq, sql } from 'drizzle-orm' import { NextRequest } from 'next/server' import { closeRedisConnection, getRedisClient } from '@/lib/core/config/redis' import { + DEADLINE_SIM_TOOL_MESSAGE, EXHAUSTED_SIM_TOOL_MESSAGE, SIM_TOOL_EXECUTION_MAX_ATTEMPTS, } from '@/lib/mothership/async-runs/execution-lease' @@ -162,7 +163,14 @@ function frameFor(streamId: string, chatId: string) { * chat, and the tool's execution lease lapsed with no result recorded. */ async function runAbandonedMidTool( - options: { attempts?: number; leaseLive?: boolean; toolName?: string; runId?: string } = {} + options: { + attempts?: number + leaseLive?: boolean + toolName?: string + runId?: string + /** Seconds left of the deadline the worker named when it first handed the call. */ + deadlineInSeconds?: number + } = {} ) { const chatId = generateId() chatIds.push(chatId) @@ -214,6 +222,11 @@ async function runAbandonedMidTool( .update(copilotAsyncToolCalls) .set({ executionAttempts: options.attempts ?? 1, + ...(options.deadlineInSeconds === undefined + ? {} + : { + executionDeadlineAt: sql`clock_timestamp() + ${options.deadlineInSeconds} * interval '1 second'`, + }), ...(options.leaseLive ? {} : { executionLeaseExpiresAt: sql`clock_timestamp() - interval '1 second'` }), @@ -238,6 +251,7 @@ async function runAbandonedMidTool( executor: 'sim', mode: 'async', arguments: TOOL_ARGS, + ...(options.deadlineInSeconds === undefined ? {} : { deadlineMs: 900_000 }), }), frame(4, 'run', { kind: 'checkpoint_pause', @@ -274,6 +288,7 @@ async function storedTool(toolCallId: string) { attempts: copilotAsyncToolCalls.executionAttempts, ownerToken: copilotAsyncToolCalls.executionOwnerToken, error: copilotAsyncToolCalls.error, + deadlineAt: copilotAsyncToolCalls.executionDeadlineAt, }) .from(copilotAsyncToolCalls) .where(eq(copilotAsyncToolCalls.toolCallId, toolCallId)) @@ -493,6 +508,29 @@ describe.runIf(Boolean(redisUrl))('taking over a run whose controller died', () expect(tool.attempts).toBe(SIM_TOOL_EXECUTION_MAX_ATTEMPTS) }) + it('fails an interrupted tool whose deadline leaves too little time to run it again', async () => { + const { streamId, runId, toolCallId } = await runAbandonedMidTool({ deadlineInSeconds: 30 }) + + await takeOverAbandonedRuns() + + expect(await waitForTerminal(runId)).toBe('complete') + const result = resumedResult(streamId) + expect(result.success).toBe(false) + expect(JSON.stringify(result)).toContain(DEADLINE_SIM_TOOL_MESSAGE) + expect(await storedTool(toolCallId)).toMatchObject({ status: 'failed', attempts: 1 }) + }) + + it('runs an interrupted tool again within what remains of its first deadline', async () => { + const { streamId, runId, toolCallId } = await runAbandonedMidTool({ deadlineInSeconds: 600 }) + const recorded = (await storedTool(toolCallId)).deadlineAt + + await takeOverAbandonedRuns() + + expect(await waitForTerminal(runId)).toBe('complete') + expectResumedSuccess(streamId) + expect(await storedTool(toolCallId)).toMatchObject({ attempts: 2, deadlineAt: recorded }) + }) + /** * A workflow tool reaches the Sim executor through the browser-or-server pickup race; the * dead controller's server run left it claimed, so only the server may pick it up again. @@ -511,6 +549,32 @@ describe.runIf(Boolean(redisUrl))('taking over a run whose controller died', () } }) + it.each([ + [30, false], + [600, true], + ])( + 'a browser claims a workflow call with %is of its deadline left: %s', + async (seconds, claimable) => { + const { runId, toolCallId } = await runAbandonedMidTool({ toolName: 'run_workflow' }) + /** Offered to a browser and not yet picked up, with `seconds` of its deadline left. */ + await db + .update(copilotAsyncToolCalls) + .set({ + claimedBy: null, + executionStartedAt: null, + executionOwnerToken: null, + executionDeadlineAt: sql`clock_timestamp() + ${seconds} * interval '1 second'`, + }) + .where(eq(copilotAsyncToolCalls.toolCallId, toolCallId)) + try { + const claim = await claimWorkflowToolExecution(toolCallId, generateId(), 'client') + expect(claim !== null).toBe(claimable) + } finally { + await db.update(copilotRuns).set({ status: 'cancelled' }).where(eq(copilotRuns.id, runId)) + } + } + ) + it('fails an exhausted workflow run before a takeover offers it to anyone', async () => { const { runId, toolCallId } = await runAbandonedMidTool({ toolName: 'run_workflow', diff --git a/apps/sim/lib/mothership/request/application/controls.ts b/apps/sim/lib/mothership/request/application/controls.ts index d09d3010092..2bed39336db 100644 --- a/apps/sim/lib/mothership/request/application/controls.ts +++ b/apps/sim/lib/mothership/request/application/controls.ts @@ -6,10 +6,6 @@ import { getActivelyBannedUserIds } from '@/lib/auth/ban' import { defineWorkspaceOperation } from '@/lib/core/application' import { defineOrganizationOperation } from '@/lib/core/application/organization-operation' import { OrchestrationError } from '@/lib/core/orchestration/types' -import { ringDesktopInbox } from '@/lib/desktop/executor/doorbell' -import { markExecutionCancelled } from '@/lib/execution/cancellation' -import { abortManualExecution } from '@/lib/execution/manual-cancellation' -import { settleStoppedRunWithoutController } from '@/lib/mothership/async-runs/orphaned-runs' import { areStreamToolExecutionsSettled, getLatestRunForStream, @@ -18,16 +14,13 @@ import { requestRunStop, } from '@/lib/mothership/async-runs/repository' import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case' -import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context' -import { appendCopilotChatMessages } from '@/lib/mothership/chat/messages-store' import { - abortActiveStream, - releasePendingChatStream, - waitForPendingChatStream, -} from '@/lib/mothership/request/session' -import { requestExplicitStreamAbort } from '@/lib/mothership/request/session/explicit-abort' + type OwnedChatContextOptions, + resolveOwnedChatContext, +} from '@/lib/mothership/chat/application/context' +import { appendCopilotChatMessages } from '@/lib/mothership/chat/messages-store' +import { deliverRunStop } from '@/lib/mothership/request/application/stop-delivery' import { requestStreamSteering } from '@/lib/mothership/request/session/steer' -import { recoverSandboxProcesses } from '@/lib/mothership/request/tools/sandbox-recovery' import { resolveActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/context' const logger = createLogger('MothershipRunControls') @@ -71,12 +64,13 @@ async function resolveRunContext({ async function resolveAdmittedRunContext( principal: SessionPrincipal, input: RunControlInput, - run: { chatId: string; workspaceId: string | null; organizationId?: string | null } + run: { chatId: string; workspaceId: string | null; organizationId?: string | null }, + options: OwnedChatContextOptions = {} ) { if (input.chatId && input.chatId !== run.chatId) { throw new OrchestrationError('forbidden', 'Stream does not belong to this chat') } - const chat = await resolveOwnedChatContext(principal, run.chatId) + const chat = await resolveOwnedChatContext(principal, run.chatId, options) if ( (run.workspaceId ?? null) !== (chat.workspaceId ?? null) || (run.organizationId ?? null) !== (chat.organizationId ?? null) @@ -100,7 +94,12 @@ async function resolveAbortContext({ input: RunControlInput }) { const run = await getLatestRunForStream(input.streamId, principal.userId) - if (run?.chatId) return resolveAdmittedRunContext(principal, input, run) + /** + * An owner may stop a run already admitted for them even after their account is blocked. A + * blocked account cannot admit a run, so a Stop recorded ahead of admission keeps the ban check. + */ + if (run?.chatId) + return resolveAdmittedRunContext(principal, input, run, { allowBlockedOwner: true }) if (input.chatId) { const chat = await resolveOwnedChatContext(principal, input.chatId) if (input.workspaceId && input.workspaceId !== chat.workspaceId) { @@ -157,69 +156,33 @@ export const abortRun = defineAuthorizedChatUseCase({ const { chatId } = await resolveAdmittedRunContext( principal, { ...input, workspaceId, organizationId }, - run + run, + { allowBlockedOwner: true } ) - /** Admission is closed: the device re-reads its inbox and cancels what it is running. */ - if (run.desktopDeviceId) ringDesktopInbox(run.desktopDeviceId, 'cancel') - /** Push delivers promptly; the worker also reconciles Sim's durable intent after an outage. */ - const workerStop = requestExplicitStreamAbort({ - streamId, - userId, - chatId, - timeoutMs: 6000, - }).catch((error) => { - logger.warn('Stop saved; worker delivery awaits reconciliation', { - streamId, - error: getErrorMessage(error), - }) - return { settled: false } - }) - await abortActiveStream(streamId) - const cancelClientWorkflows = async () => { - const executionIds = await getUnsettledClientWorkflowExecutions(streamId, userId) - await Promise.all( - executionIds.map(async (executionId) => { - /** The execution route reserved these IDs before recording ownership; no client result is trusted. */ - abortManualExecution(executionId) - await markExecutionCancelled(executionId) + /** Read alongside the signals, which need none of it. */ + const work = Promise.all([ + getUnsettledClientWorkflowExecutions(streamId, userId).catch((error) => { + logger.warn('Stopped stream workflow ownership could not be read', { + streamId, + error: getErrorMessage(error), }) - ) - } - const recoverCommands = async () => { - const signal = AbortSignal.timeout(8000) - const processes = await getUnsettledStreamSandboxProcesses(streamId, userId).catch( - (error) => { - logger.warn('Stopped stream sandbox ownership could not be read', { - streamId, - error: getErrorMessage(error), - }) - return [] - } - ) - await recoverSandboxProcesses(processes, signal) - } - const [settled, worker] = await Promise.all([ - waitForPendingChatStream(chatId, 8000, streamId), - workerStop, - recoverCommands(), - cancelClientWorkflows().catch((error) => { - logger.warn('Stopped stream workflow cancellation could not be delivered', { + return [] + }), + getUnsettledStreamSandboxProcesses(streamId, userId).catch((error) => { + logger.warn('Stopped stream sandbox ownership could not be read', { streamId, error: getErrorMessage(error), }) + return [] }), - ]) - if (!settled) { - await releasePendingChatStream(chatId, streamId) - logger.warn('Stopped stream did not settle; released its chat lock', { chatId, streamId }) - } - /** A run with no controller left, or none to begin with, has nothing else to settle it. */ - await settleStoppedRunWithoutController(run.id).catch((error) => { - logger.warn('Stopped run without a controller could not be settled', { - streamId, - error: getErrorMessage(error), - }) - }) + ]).then(([clientWorkflowExecutionIds, sandboxProcesses]) => ({ + clientWorkflowExecutionIds, + sandboxProcesses, + })) + const { settled, workerSettled } = await deliverRunStop( + { id: run.id, streamId, userId, chatId, desktopDeviceId: run.desktopDeviceId }, + work + ) if (!settled) return { aborted: true, settled: false, forceReleased: true } const toolsSettled = await areStreamToolExecutionsSettled(streamId, userId).catch((error) => { logger.warn('Stopped stream tool settlement could not be verified', { @@ -228,7 +191,7 @@ export const abortRun = defineAuthorizedChatUseCase({ }) return false }) - return { aborted: true, settled: worker.settled && toolsSettled } + return { aborted: true, settled: workerSettled && toolsSettled } }, }) diff --git a/apps/sim/lib/mothership/request/application/read-control.test.ts b/apps/sim/lib/mothership/request/application/read-control.test.ts index 63635540529..1cf0ed037d1 100644 --- a/apps/sim/lib/mothership/request/application/read-control.test.ts +++ b/apps/sim/lib/mothership/request/application/read-control.test.ts @@ -68,36 +68,87 @@ describe('organization run-control authorization', () => { }) ) }) - it.each(['user', 'organization', 'chat', 'expired'] as const)( - 'rejects altered %s delegation', - async (field) => { + it.each(['chat', 'expired'] as const)('rejects altered %s delegation', async (field) => { + queueChat() + const trusted = principal() + const caller = { + ...trusted, + ...(field === 'chat' ? { resourceScope: { chatId: 'other' } } : {}), + ...(field === 'expired' ? { expiresAt: new Date(0) } : {}), + } + await expect(readRunControl.execute({ principal: caller, input })).rejects.toThrow() + expect(mocks.stopped).not.toHaveBeenCalled() + }) + it('answers Stop without reading the owner state for a user the chat does not belong to', async () => { + queueChat() + const caller = { ...principal(), subjectUserId: 'other' } + expect(await readRunControl.execute({ principal: caller, input })).toEqual({ stopped: true }) + expect(mocks.stopped).not.toHaveBeenCalled() + }) + it('rejects a delegation for another organization instead of answering Stop', async () => { + queueChat() + const caller = { ...principal(), organizationId: 'other' } + await expect(readRunControl.execute({ principal: caller, input })).rejects.toThrow( + 'Organization delegation is no longer valid' + ) + expect(mocks.stopped).not.toHaveBeenCalled() + }) + it('rejects a delegation that expires while the request reads the chat instead of answering Stop', async () => { + vi.useFakeTimers({ toFake: ['Date'] }) + try { queueChat() - const trusted = principal() - const caller = { - ...trusted, - ...(field === 'user' ? { subjectUserId: 'other' } : {}), - ...(field === 'organization' ? { organizationId: 'other' } : {}), - ...(field === 'chat' ? { resourceScope: { chatId: 'other' } } : {}), - ...(field === 'expired' ? { expiresAt: new Date(0) } : {}), - } - await expect(readRunControl.execute({ principal: caller, input })).rejects.toThrow() + const caller = principal() + mocks.banned.mockImplementation(async () => { + vi.setSystemTime(caller.expiresAt.getTime() + 1) + return [] + }) + await expect(readRunControl.execute({ principal: caller, input })).rejects.toThrow( + 'Organization delegation is no longer valid' + ) expect(mocks.stopped).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() } - ) - it('rejects removed organization membership and a run owned by another organization', async () => { + }) + it('lets a run continue when the member’s group withholds Copilot', async () => { + queueChat() + permissionGroupsResolveMockFns.mockGetUserPermissionConfigForOrganization.mockResolvedValue({ + hideCopilot: true, + }) + mocks.stopped.mockResolvedValue(false) + expect(await readRunControl.execute({ principal: principal(), input })).toEqual({ + stopped: false, + }) + }) + it('answers Stop once organization membership ends or the run belongs to another organization', async () => { queueTableRows(copilotChats, [ { userId: 'actor', organizationId: 'org-1', workspaceId: null, type: 'mothership' }, ]) - await expect(readRunControl.execute({ principal: principal(), input })).rejects.toThrow( - 'Organization not found' - ) + expect(await readRunControl.execute({ principal: principal(), input })).toEqual({ + stopped: true, + }) expect(mocks.stopped).not.toHaveBeenCalled() resetDbChainMock() queueChat() mocks.run.mockResolvedValue({ chatId: 'chat-1', organizationId: 'org-2', workspaceId: null }) - await expect(readRunControl.execute({ principal: principal(), input })).rejects.toThrow( - 'Stream not found' - ) + expect(await readRunControl.execute({ principal: principal(), input })).toEqual({ + stopped: true, + }) + expect(mocks.stopped).not.toHaveBeenCalled() + }) + it('answers Stop for a suspended owner', async () => { + queueChat() + mocks.banned.mockResolvedValue(['actor']) + expect(await readRunControl.execute({ principal: principal(), input })).toEqual({ + stopped: true, + }) expect(mocks.stopped).not.toHaveBeenCalled() }) + it('lets a live run continue', async () => { + queueChat() + mocks.stopped.mockResolvedValue(false) + expect(await readRunControl.execute({ principal: principal(), input })).toEqual({ + stopped: false, + }) + }) }) diff --git a/apps/sim/lib/mothership/request/application/read-control.ts b/apps/sim/lib/mothership/request/application/read-control.ts index b882ca60fd5..44b909bd767 100644 --- a/apps/sim/lib/mothership/request/application/read-control.ts +++ b/apps/sim/lib/mothership/request/application/read-control.ts @@ -1,11 +1,22 @@ -import type { DelegatedPrincipal, OrganizationDelegatedPrincipal } from '@sim/auth/principal' -import { defineWorkspaceOperation } from '@/lib/core/application' +import type { + DelegatedPrincipal, + OrganizationDelegatedPrincipal, + Principal, +} from '@sim/auth/principal' +import { + type AuthorizingUseCase, + DelegatedServiceAuthorizationError, + DelegatedWorkspaceAuthorizationError, + defineWorkspaceOperation, + PrincipalKindAuthorizationError, +} from '@/lib/core/application' +import { OrganizationDelegationAuthorizationError } from '@/lib/core/application/organization-authorization' import { defineOrganizationOperation } from '@/lib/core/application/organization-operation' -import { OrchestrationError } from '@/lib/core/orchestration/types' +import { asOrchestrationError, OrchestrationError } from '@/lib/core/orchestration/types' import { getLatestRunForStream, isRunStopRequested } from '@/lib/mothership/async-runs/repository' import { defineAuthorizedChatUseCase } from '@/lib/mothership/chat/application/authorized-chat-use-case' import { resolveOwnedChatContext } from '@/lib/mothership/chat/application/context' -import type { RunControlRequest } from '@/lib/mothership/generated/run-control' +import type { RunControlRequest, RunControlState } from '@/lib/mothership/generated/run-control' export const RUN_CONTROL_AUDIENCE = 'sim:copilot-run-control' // permission-group-exempt: observing stop state must remain possible after Copilot access is withheld @@ -19,7 +30,8 @@ export const readRunControlOperation = defineWorkspaceOperation({ delegatedServices: ['copilot'], }) -export const readRunControl = defineAuthorizedChatUseCase({ +/** Answers for a run Sim still has: its chat live, its owner still allowed, Stop not requested. */ +const readLiveRunControl = defineAuthorizedChatUseCase({ operation: readRunControlOperation, /** permission-group-exempt: observing Stop must remain available after Assistant access is withheld. */ organizationOperation: defineOrganizationOperation({ @@ -60,3 +72,68 @@ export const readRunControl = defineAuthorizedChatUseCase({ return { stopped: await isRunStopRequested({ ...context, streamId: input.streamId }) } }, }) + +/** + * A refusal of the caller's delegation itself (expired mid-request, or out of scope), which + * says nothing about whether the run may continue. + */ +function isCallerRefusal(error: OrchestrationError): boolean { + return ( + error instanceof DelegatedWorkspaceAuthorizationError || + error instanceof DelegatedServiceAuthorizationError || + error instanceof PrincipalKindAuthorizationError || + error instanceof OrganizationDelegationAuthorizationError + ) +} + +/** The worker asking about its own run: a copilot delegation for this chat, still in date. */ +function requireRunControlCaller(principal: Principal, input: RunControlRequest): void { + if (principal.kind !== 'delegated' && principal.kind !== 'organization_delegated') + throw new OrchestrationError('forbidden', 'Run control delegation is not valid') + /** A workspace delegation may leave the chat unscoped; an organization one always names it. */ + const chatScope = + principal.kind === 'delegated' + ? (principal.resourceScope?.chatId ?? input.chatId) + : principal.serviceId === 'copilot' + ? principal.resourceScope.chatId + : undefined + if ( + principal.serviceId !== 'copilot' || + principal.audience !== RUN_CONTROL_AUDIENCE || + principal.expiresAt.getTime() <= Date.now() || + chatScope !== input.chatId + ) { + throw new OrchestrationError('forbidden', 'Run control delegation is not valid') + } +} + +/** + * Whether the worker's run may continue. Sim is the authority, so a run whose chat was + * deleted, whose owner or workspace is gone, or whose owner lost access answers Stop rather + * than an error the worker can only retry: a deletion stops its runs even when no push + * reached the worker. Only a failure to read the answer is an error. + */ +export const readRunControl: AuthorizingUseCase< + typeof readRunControlOperation, + RunControlRequest, + RunControlState +> = { + operation: readRunControlOperation, + async authorize({ principal, input }) { + requireRunControlCaller(principal, input) + }, + async execute(args) { + requireRunControlCaller(args.principal, args.input) + try { + return await readLiveRunControl.execute(args) + } catch (error) { + const refusal = asOrchestrationError(error) + if ( + (refusal?.code === 'not_found' || refusal?.code === 'forbidden') && + !isCallerRefusal(refusal) + ) + return { stopped: true } + throw error + } + }, +} diff --git a/apps/sim/lib/mothership/request/application/removal-stops-runs.integration.ts b/apps/sim/lib/mothership/request/application/removal-stops-runs.integration.ts new file mode 100644 index 00000000000..940c46f0190 --- /dev/null +++ b/apps/sim/lib/mothership/request/application/removal-stops-runs.integration.ts @@ -0,0 +1,1165 @@ +/** + * Deleting a chat, an account or a workspace, or removing someone's access, stops the runs it + * leaves without an owner, against real PostgreSQL and Redis through the production deletion + * paths and the worker's run-control read. A local HTTP server stands in for the worker and + * records each Stop pushed to it. + */ +import type { Principal } from '@sim/auth/principal' +import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' +import { createDeferred } from '@sim/testing/helpers/deferred' +import { authMock, authMockFns } from '@sim/testing/mocks/auth.mock' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { redisUrl, inheritedEnv, worker } = await vi.hoisted(async () => { + const { readTestRedisUrl } = await import('@sim/db/testing/test-infrastructure') + const { createServer } = await import('node:http') + const worker = { + stops: [] as string[], + /** Streams whose Stop the worker cannot take, as during an outage. */ + unreachable: new Set(), + } + const server = createServer(async (request, response) => { + let raw = '' + for await (const chunk of request) raw += chunk + const body = raw ? JSON.parse(raw) : {} + const streamId = String(body.messageId ?? '') + if (request.url !== '/api/streams/explicit-abort' || worker.unreachable.has(streamId)) { + response.writeHead(503, { 'content-type': 'application/json' }) + response.end(JSON.stringify({ error: 'Unavailable' })) + return + } + worker.stops.push(streamId) + response.writeHead(200, { 'content-type': 'application/json' }) + response.end(JSON.stringify({ settled: true })) + }) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() as { port: number } + const url = readTestRedisUrl() + const inheritedEnv = { + REDIS_URL: process.env.REDIS_URL, + SIM_AGENT_API_URL: process.env.SIM_AGENT_API_URL, + } + if (url) process.env.REDIS_URL = url + process.env.SIM_AGENT_API_URL = `http://127.0.0.1:${port}` + return { redisUrl: url, inheritedEnv, worker: Object.assign(worker, { server }) } +}) + +vi.mock('@/lib/auth', () => authMock) + +/** Every stream a Stop delivery was started for, recorded when delivery is requested. */ +const requestedDeliveries = vi.hoisted(() => [] as string[]) +vi.mock('@/lib/mothership/request/application/stop-delivery', async (importOriginal) => { + const actual = + await importOriginal() + return { + ...actual, + deliverRunStops: (...args: Parameters) => { + requestedDeliveries.push(...args[0].map((run) => run.streamId)) + actual.deliverRunStops(...args) + }, + } +}) + +import { db } from '@sim/db' +import { + copilotAsyncToolCalls, + copilotChats, + copilotRequestStops, + copilotRuns, + idempotencyKey, + member, + organization, + permissions, + scimConnection, + scimGroupMapping, + scimGroupMember, + user, + workspace, +} from '@sim/db/schema' +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' +import { sleep } from '@sim/utils/helpers' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray, sql } from 'drizzle-orm' +import { NextRequest } from 'next/server' +import { closeRedisConnection, getRedisClient } from '@/lib/core/config/redis' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { SIM_TOOL_EXECUTION_VERSION } from '@/lib/mothership/async-runs/lifecycle' +import { createRunSegment } from '@/lib/mothership/async-runs/repository' +import { admitChatTurn } from '@/lib/mothership/chat/application/admit-turn' +import { deleteCopilotChat, softDeleteMothershipChat } from '@/lib/mothership/chat/deletion' +import { chatPubSub } from '@/lib/mothership/chat-status' +import { abortRun, steerRun } from '@/lib/mothership/request/application/controls' +import { claimRunController } from '@/lib/mothership/request/lifecycle/controller-ownership' +import { + acquirePendingChatStream, + getLocalChatStreamLease, + releasePendingChatStream, +} from '@/lib/mothership/request/session/abort' +import { chatStreamLockKey } from '@/lib/mothership/request/session/controller-lease' +import { executeSimControl } from '@/lib/mothership/transport/control' +import { + removeOrganizationMemberRecord, + updateOrganizationMemberRecord, +} from '@/lib/organizations/member-manager' +import { changeMemberRoleTx } from '@/lib/organizations/members/lifecycle' +import { AccountDeletionBlockedError, deleteUserAccount } from '@/lib/users/account-deletion' +import { revokeWorkspaceAccessTx } from '@/lib/workspaces/access/workspace-access' +import { removeWorkspaceMemberRecord } from '@/lib/workspaces/permissions/member-removal-store' +import { DELETE as deleteWorkspace } from '@/app/api/workspaces/[id]/route' +import { replaceScimGroup } from '@/ee/scim/lib/application/groups/manage-groups' +import { + PROJECTION_BATCH_SIZE, + reconcileUsersProjectionInBatches, +} from '@/ee/scim/lib/projection/reconcile-user' +import { insertScimGroup } from '@/ee/scim/lib/repository/groups' +import { insertScimUser } from '@/ee/scim/lib/repository/users' + +const userIds: string[] = [] +const workspaceIds: string[] = [] +const organizationIds: string[] = [] + +interface SeededRun { + userId: string + chatId: string + streamId: string + runId: string + workspaceId?: string + organizationId?: string +} + +async function seedUser() { + const id = generateId() + userIds.push(id) + const now = new Date() + await db.insert(user).values({ + id, + name: 'Run removal fixture', + email: `${id}@run-removal.test`, + emailVerified: true, + createdAt: now, + updatedAt: now, + }) + return id +} + +/** A workspace its owner pays for, with each listed member granted access. */ +async function seedWorkspace(ownerId: string, members: string[] = []) { + const id = generateId() + workspaceIds.push(id) + await insertWorkspaceFixture(db, { + id, + name: 'Run removal fixture', + ownerId, + billedAccountUserId: ownerId, + }) + await db.insert(permissions).values( + [ownerId, ...members].map((userId) => ({ + id: generateId(), + userId, + entityType: 'workspace', + entityId: id, + permissionType: userId === ownerId ? ('admin' as const) : ('write' as const), + })) + ) + return id +} + +/** An organization its owner runs, with one workspace of its own. */ +async function seedOrganization(ownerId: string) { + const organizationId = generateId() + organizationIds.push(organizationId) + await db + .insert(organization) + .values({ id: organizationId, name: 'Run removal fixture', slug: organizationId }) + await db + .insert(member) + .values({ id: generateId(), organizationId, userId: ownerId, role: 'owner' }) + const workspaceId = generateId() + workspaceIds.push(workspaceId) + await insertWorkspaceFixture(db, { + id: workspaceId, + name: 'Run removal fixture', + ownerId, + billedAccountUserId: ownerId, + organizationId, + }) + return { organizationId, workspaceId } +} + +async function grantWrite(userId: string, workspaceId: string) { + await db.insert(permissions).values({ + id: generateId(), + userId, + entityType: 'workspace', + entityId: workspaceId, + permissionType: 'write', + }) +} + +/** A Chat run its controller is still driving, in a chat of its own. */ +async function seedRun( + owner: { userId: string } & ({ workspaceId: string } | { organizationId: string }), + type: 'mothership' | 'copilot' = 'mothership' +): Promise { + const chatId = generateId() + const streamId = generateId() + testStreams.add(streamId) + const runId = generateId() + const scope = + 'workspaceId' in owner + ? { workspaceId: owner.workspaceId } + : { organizationId: owner.organizationId } + await db.insert(copilotChats).values({ + id: chatId, + userId: owner.userId, + type, + conversationId: streamId, + ...scope, + }) + await db.insert(copilotRuns).values({ + id: runId, + executionId: generateId(), + chatId, + userId: owner.userId, + streamId, + status: 'active', + toolExecutionVersion: SIM_TOOL_EXECUTION_VERSION, + requestContext: { requestId: generateId(), controllerToken: `${streamId}\n${generateId()}` }, + ...scope, + }) + return { userId: owner.userId, chatId, streamId, runId, ...scope } +} + +/** A chat with no run yet, as a task wake, inbox email, API or Slack turn finds it. */ +async function seedChat(userId: string, workspaceId: string) { + const chatId = generateId() + await db.insert(copilotChats).values({ id: chatId, userId, workspaceId, type: 'mothership' }) + return chatId +} + +/** Admits a headless turn's run the way those surfaces do, without the chat view's admission. */ +async function admitHeadlessRun(userId: string, chatId: string, workspaceId: string) { + const streamId = generateId() + testStreams.add(streamId) + const run = await createRunSegment({ + id: generateId(), + executionId: generateId(), + chatId, + userId, + workspaceId, + streamId, + requestContext: { source: 'headless_lifecycle' }, + }) + return { run, seeded: { userId, chatId, streamId, runId: run.id, workspaceId } } +} + +async function toolAdmissionClosedAt(run: SeededRun) { + const [row] = await db + .select({ closedAt: copilotRuns.toolAdmissionClosedAt }) + .from(copilotRuns) + .where(eq(copilotRuns.id, run.runId)) + return row?.closedAt ?? null +} + +/** What the worker's heartbeat reads, through the outbound control transport. */ +async function runControl(run: SeededRun) { + const result = await executeSimControl({ + id: generateId(), + expiresAt: Date.now() + 15_000, + scope: { + userId: run.userId, + chatId: run.chatId, + ...(run.workspaceId ? { workspaceId: run.workspaceId } : {}), + ...(run.organizationId ? { organizationId: run.organizationId } : {}), + }, + operation: { kind: 'run_control', input: { chatId: run.chatId, streamId: run.streamId } }, + }) + return { status: result.status, body: JSON.parse(result.body) } +} + +async function hasStop(run: SeededRun) { + const rows = await db + .select({ streamId: copilotRequestStops.streamId }) + .from(copilotRequestStops) + .where( + and( + eq(copilotRequestStops.userId, run.userId), + eq(copilotRequestStops.streamId, run.streamId) + ) + ) + return rows.length > 0 +} + +async function runStatus(run: SeededRun) { + const [row] = await db + .select({ status: copilotRuns.status }) + .from(copilotRuns) + .where(eq(copilotRuns.id, run.runId)) + return row?.status +} + +/** This test's streams: a removal's delivery runs after it returns, and can outlive its test. */ +const testStreams = new Set() + +/** The Stops the worker received for this test's runs. */ +const pushedStops = () => worker.stops.filter((streamId) => testStreams.has(streamId)).sort() + +/** The backend queued on a lock behind any of these, once one is. */ +async function lockWaiterBehind(...blockers: number[]) { + const pids = sql`ARRAY[${sql.join( + blockers.map((pid) => sql`${pid}::int`), + sql`, ` + )}]` + let waiter: number | undefined + await expect + .poll( + async () => { + const [row] = await db.execute<{ pid: number }>(sql` + SELECT pid FROM pg_stat_activity WHERE datname = current_database() + AND wait_event_type = 'Lock' AND pid <> ALL(${pids}) + AND pg_blocking_pids(pid) && ${pids} LIMIT 1 + `) + waiter = row?.pid + return waiter + }, + { interval: 5, timeout: 5000 } + ) + .toBeDefined() + return waiter as number +} + +/** Holds the chat row locked while `run` queues work behind it, then commits. */ +async function whileHolding(chatId: string, run: (holder: number) => Promise): Promise { + const locked = createDeferred() + const release = createDeferred() + const holding = db.transaction(async (tx) => { + await tx + .select({ id: copilotChats.id }) + .from(copilotChats) + .where(eq(copilotChats.id, chatId)) + .for('update') + const [backend] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`) + locked.resolve(backend.pid) + await release.promise + }) + holding.catch(locked.reject) + const holder = await locked.promise + try { + return await run(holder) + } finally { + release.resolve() + await holding + } +} + +type DirectoryTarget = { workspaceId: string } | { role: 'admin' } + +/** Points the group's only mapping at `target`, as an administrator's mapping edit does. */ +async function mapGroup(groupId: string, target: DirectoryTarget) { + await db.delete(scimGroupMapping).where(eq(scimGroupMapping.groupId, groupId)) + await db.insert(scimGroupMapping).values({ + id: generateId(), + groupId, + ...('workspaceId' in target + ? { + targetKind: 'workspace', + workspaceId: target.workspaceId, + permissionType: 'write' as const, + } + : { targetKind: 'org_role', role: target.role }), + }) +} + +/** A directory connection for the organization, with one group mapped to `target`. */ +async function seedDirectory(organizationId: string, target: DirectoryTarget) { + const connectionId = generateId() + await db.insert(scimConnection).values({ id: connectionId, organizationId }) + const group = await insertScimGroup(db, { connectionId, displayName: generateId() }) + await mapGroup(group.id, target) + const principal: Principal = { + kind: 'scim_connection', + organizationId, + connectionId, + credentialId: generateId(), + scopes: ['groups:write'], + } + /** Sets the group's members, as a directory's full-group PUT does. */ + const setGroupMembers = (memberIds: string[]) => + replaceScimGroup.execute({ + principal, + input: { groupId: group.id, group: { displayName: group.displayName, memberIds } }, + }) + /** Re-projects these users in bounded transactions, as the scheduled pass and mapping edits do. */ + const reconcile = (scimUserIds: string[]) => + reconcileUsersProjectionInBatches({ connectionId, organizationId, scimUserIds, settings: {} }) + return { + connectionId, + groupId: group.id, + setGroupMembers, + reconcile, + remap: (next: DirectoryTarget) => mapGroup(group.id, next), + } +} + +/** An organization member the directory provisions, returning their directory user id. */ +async function seedDirectoryMember(connectionId: string, organizationId: string) { + const userId = await seedUser() + await db.insert(member).values({ id: generateId(), organizationId, userId, role: 'member' }) + const { id } = await insertScimUser(db, { + connectionId, + userId, + attributes: { + userName: `${userId}@run-removal.test`, + active: true, + name: { formatted: 'Run removal fixture' }, + emails: [{ value: `${userId}@run-removal.test`, primary: true }], + }, + active: true, + }) + return { userId, scimUserId: id } +} + +/** Fails, inside its transaction, any projection that records a grant for this directory user. */ +async function refuseGrantsFor(scimUserId: string) { + const name = `refuse_grant_${scimUserId.replaceAll('-', '_')}` + await db.execute( + sql.raw(`CREATE FUNCTION ${name}() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN RAISE EXCEPTION 'projection refused'; END $$`) + ) + await db.execute( + sql.raw(`CREATE TRIGGER ${name} BEFORE INSERT ON scim_projection_grant FOR EACH ROW + WHEN (NEW.scim_user_id = '${scimUserId}') EXECUTE FUNCTION ${name}()`) + ) + return async () => { + await db.execute(sql.raw(`DROP TRIGGER ${name} ON scim_projection_grant`)) + await db.execute(sql.raw(`DROP FUNCTION ${name}()`)) + } +} + +const REFUSED = { cause: { message: 'projection refused' } } + +/** Directory members, ordered as a projection visits them. */ +async function seedDirectoryMembersInOrder( + connectionId: string, + organizationId: string, + count: number +) { + const members = [] + for (let index = 0; index < count; index += 1) { + members.push(await seedDirectoryMember(connectionId, organizationId)) + } + return members.sort((left, right) => (left.scimUserId < right.scimUserId ? -1 : 1)) +} + +/** Stop deliveries started for this test's runs; one is started before its write returns. */ +const requestedStops = () => + requestedDeliveries.filter((streamId) => testStreams.has(streamId)).sort() + +/** Queues admission and deletion behind a held chat row in the order `first` names. */ +async function queueOnChat( + chatId: string, + first: 'admission' | 'deletion', + admit: () => Promise, + remove: () => Promise +): Promise<{ admission: Promise; deletion: Promise }> { + return whileHolding(chatId, async (holder) => { + const admission = first === 'admission' ? admit() : undefined + const deletion = admission ? undefined : remove() + const waiter = await lockWaiterBehind(holder) + const queued = { admission: admission ?? admit(), deletion: deletion ?? remove() } + await lockWaiterBehind(holder, waiter) + return queued + }) +} + +const STOPPED = { status: 200, body: { stopped: true } } +const LIVE = { status: 200, body: { stopped: false } } + +/** Runs whether or not the suite does, so a skipped suite never leaks the worker or env. */ +afterAll(async () => { + await new Promise((resolve) => worker.server.close(() => resolve())) + for (const [key, value] of Object.entries(inheritedEnv)) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } +}) + +describe.runIf(Boolean(redisUrl))('removing a run’s chat, owner or access stops the run', () => { + beforeEach(() => { + testStreams.clear() + worker.unreachable.clear() + }) + + afterAll(async () => { + if (userIds.length) { + await db.delete(copilotRuns).where(inArray(copilotRuns.userId, userIds)) + await db.delete(copilotChats).where(inArray(copilotChats.userId, userIds)) + await db.delete(permissions).where(inArray(permissions.userId, userIds)) + await db.delete(member).where(inArray(member.userId, userIds)) + } + if (workspaceIds.length) await deleteWorkspaceFixture(db, inArray(workspace.id, workspaceIds)) + if (organizationIds.length) + await db.delete(organization).where(inArray(organization.id, organizationIds)) + if (userIds.length) await db.delete(user).where(inArray(user.id, userIds)) + chatPubSub?.dispose() + await closeRedisConnection() + }) + + it('lets a run continue while its chat, owner and access remain', async () => { + const ownerId = await seedUser() + const run = await seedRun({ userId: ownerId, workspaceId: await seedWorkspace(ownerId) }) + + expect(await runControl(run)).toEqual(LIVE) + }) + + it('stops a deleted Mothership chat’s run at once, and a restore does not revive it', async () => { + const ownerId = await seedUser() + const workspaceId = await seedWorkspace(ownerId) + const run = await seedRun({ userId: ownerId, workspaceId }) + const sibling = await seedRun({ userId: ownerId, workspaceId }) + + expect(await softDeleteMothershipChat({ chatId: run.chatId, userId: ownerId })).toEqual({ + workspaceId, + organizationId: null, + }) + + await expect.poll(pushedStops).toEqual([run.streamId]) + expect(await hasStop(run)).toBe(true) + await expect.poll(() => runStatus(run)).toBe('cancelled') + expect(await runControl(run)).toEqual(STOPPED) + await db.update(copilotChats).set({ deletedAt: null }).where(eq(copilotChats.id, run.chatId)) + expect(await runControl(run)).toEqual(STOPPED) + expect(await hasStop(sibling)).toBe(false) + expect(await runControl(sibling)).toEqual(LIVE) + }) + + it('stops a permanently deleted copilot chat’s run', async () => { + const ownerId = await seedUser() + const run = await seedRun( + { userId: ownerId, workspaceId: await seedWorkspace(ownerId) }, + 'copilot' + ) + + expect(await deleteCopilotChat({ chatId: run.chatId, userId: ownerId })).not.toBeNull() + + await expect.poll(pushedStops).toEqual([run.streamId]) + expect(await runControl(run)).toEqual(STOPPED) + }) + + it('cancels the client workflow a permanently deleted chat’s run started', async () => { + const ownerId = await seedUser() + const run = await seedRun( + { userId: ownerId, workspaceId: await seedWorkspace(ownerId) }, + 'copilot' + ) + const executionId = generateId() + await db.insert(copilotAsyncToolCalls).values({ + runId: run.runId, + toolCallId: generateId(), + toolName: 'run_workflow', + clientWorkflowExecutionId: executionId, + executionStartedAt: new Date(), + }) + + expect(await deleteCopilotChat({ chatId: run.chatId, userId: ownerId })).not.toBeNull() + + await expect.poll(() => getRedisClient()?.exists(`execution:cancel:${executionId}`)).toBe(1) + }) + + it('still stops the run when the worker cannot take the pushed Stop', async () => { + const ownerId = await seedUser() + const run = await seedRun({ userId: ownerId, workspaceId: await seedWorkspace(ownerId) }) + worker.unreachable.add(run.streamId) + + expect(await softDeleteMothershipChat({ chatId: run.chatId, userId: ownerId })).not.toBeNull() + + expect(pushedStops()).toEqual([]) + expect(await runControl(run)).toEqual(STOPPED) + }) + + it('never lets a takeover adopt a run whose chat was deleted', async () => { + const ownerId = await seedUser() + const run = await seedRun({ userId: ownerId, workspaceId: await seedWorkspace(ownerId) }) + const [stored] = await db + .select({ requestContext: copilotRuns.requestContext }) + .from(copilotRuns) + .where(eq(copilotRuns.id, run.runId)) + await db + .update(copilotChats) + .set({ deletedAt: new Date() }) + .where(eq(copilotChats.id, run.chatId)) + + const claimed = await claimRunController({ + runId: run.runId, + chatId: run.chatId, + previousToken: String((stored.requestContext as { controllerToken: string }).controllerToken), + token: `${run.streamId}\n${generateId()}`, + recoveryBackoff: { attempts: 1, claimedAt: Date.now(), notBefore: Date.now() }, + }) + + expect(claimed).toBe(false) + }) + + it('stops a deleted account’s runs in every workspace it erases, and no one else’s', async () => { + const ownerId = await seedUser() + const departingId = await seedUser() + const first = await seedRun({ + userId: departingId, + workspaceId: await seedWorkspace(departingId), + }) + const second = await seedRun({ + userId: departingId, + workspaceId: await seedWorkspace(departingId), + }) + const bystander = await seedRun({ userId: ownerId, workspaceId: await seedWorkspace(ownerId) }) + + await deleteUserAccount(departingId) + + await expect.poll(pushedStops).toEqual([first.streamId, second.streamId].sort()) + expect(await runControl(first)).toEqual(STOPPED) + expect(await runControl(second)).toEqual(STOPPED) + expect(await runControl(bystander)).toEqual(LIVE) + }) + + it('stops nothing when the account deletion is refused', async () => { + const ownerId = await seedUser() + const departingId = await seedUser() + const run = await seedRun({ + userId: departingId, + workspaceId: await seedWorkspace(ownerId, [departingId]), + }) + + await expect(deleteUserAccount(departingId)).rejects.toBeInstanceOf(AccountDeletionBlockedError) + + expect(pushedStops()).toEqual([]) + expect(await hasStop(run)).toBe(false) + expect(await runControl(run)).toEqual(LIVE) + }) + + it('stops every member’s runs in an archived workspace and none elsewhere', async () => { + const ownerId = await seedUser() + const memberId = await seedUser() + const archived = await seedWorkspace(ownerId, [memberId]) + const ownerRun = await seedRun({ userId: ownerId, workspaceId: archived }) + const memberRun = await seedRun({ userId: memberId, workspaceId: archived }) + const elsewhere = await seedRun({ + userId: memberId, + workspaceId: await seedWorkspace(memberId), + }) + + await seedWorkspace(ownerId) + authMockFns.mockGetSession.mockResolvedValue({ + user: { id: ownerId }, + session: { id: generateId() }, + }) + + const response = await deleteWorkspace( + new NextRequest(`http://localhost/api/workspaces/${archived}`, { + method: 'DELETE', + body: '{}', + }), + { params: Promise.resolve({ id: archived }) } + ) + + expect(response.status).toBe(200) + + await expect.poll(pushedStops).toEqual([ownerRun.streamId, memberRun.streamId].sort()) + expect(await runControl(ownerRun)).toEqual(STOPPED) + expect(await runControl(memberRun)).toEqual(STOPPED) + expect(await runControl(elsewhere)).toEqual(LIVE) + }) + + it('stops a removed member’s runs in that workspace only', async () => { + const ownerId = await seedUser() + const memberId = await seedUser() + const left = await seedWorkspace(ownerId, [memberId]) + const removedRun = await seedRun({ userId: memberId, workspaceId: left }) + const ownerRun = await seedRun({ userId: ownerId, workspaceId: left }) + const elsewhere = await seedRun({ + userId: memberId, + workspaceId: await seedWorkspace(memberId), + }) + + await removeWorkspaceMemberRecord(left, memberId, ownerId) + + await expect.poll(pushedStops).toEqual([removedRun.streamId]) + expect(await hasStop(removedRun)).toBe(true) + expect(await runControl(removedRun)).toEqual(STOPPED) + expect(await runControl(ownerRun)).toEqual(LIVE) + expect(await runControl(elsewhere)).toEqual(LIVE) + }) + + it('keeps an organization admin’s run when only their explicit grant is withdrawn', async () => { + const ownerId = await seedUser() + const adminId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + await db + .insert(member) + .values({ id: generateId(), organizationId, userId: adminId, role: 'admin' }) + await grantWrite(adminId, workspaceId) + const run = await seedRun({ userId: adminId, workspaceId }) + + const revocation = await db.transaction((tx) => + revokeWorkspaceAccessTx(tx, { workspaceId, userId: adminId }) + ) + + expect(revocation).toMatchObject({ revoked: true, stoppedRuns: [] }) + expect(await hasStop(run)).toBe(false) + expect(await runControl(run)).toEqual(LIVE) + }) + + it('stops an organization admin’s runs once a demotion ends their derived access', async () => { + const ownerId = await seedUser() + const adminId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + await db + .insert(member) + .values({ id: generateId(), organizationId, userId: adminId, role: 'admin' }) + const derived = await seedRun({ userId: adminId, workspaceId }) + const granted = await seedRun({ userId: adminId, workspaceId: await seedWorkspace(ownerId) }) + await grantWrite(adminId, granted.workspaceId ?? '') + + await updateOrganizationMemberRecord({ + organizationId, + userId: adminId, + role: 'member', + actorUserId: ownerId, + }) + + await expect.poll(pushedStops).toEqual([derived.streamId]) + expect(await runControl(derived)).toEqual(STOPPED) + expect(await runControl(granted)).toEqual(LIVE) + }) + + it('records Stop when a directory withdraws an admin’s grant and then their admin role', async () => { + const ownerId = await seedUser() + const adminId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + await db + .insert(member) + .values({ id: generateId(), organizationId, userId: adminId, role: 'admin' }) + await grantWrite(adminId, workspaceId) + const run = await seedRun({ userId: adminId, workspaceId }) + + await db.transaction(async (tx) => { + await revokeWorkspaceAccessTx(tx, { workspaceId, userId: adminId }) + await changeMemberRoleTx(tx, { organizationId, userId: adminId, role: 'member' }) + }) + + expect(await hasStop(run)).toBe(true) + expect(await runControl(run)).toEqual(STOPPED) + }) + + it('pushes Stop for a directory group removal once its transaction commits', async () => { + const ownerId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + const directory = await seedDirectory(organizationId, { workspaceId }) + const removed = await seedDirectoryMember(directory.connectionId, organizationId) + await directory.setGroupMembers([removed.scimUserId]) + const run = await seedRun({ userId: removed.userId, workspaceId }) + const ownerRun = await seedRun({ userId: ownerId, workspaceId }) + + await directory.setGroupMembers([]) + + await expect.poll(pushedStops).toEqual([run.streamId]) + expect(await runControl(run)).toEqual(STOPPED) + expect(await runControl(ownerRun)).toEqual(LIVE) + }) + + it('pushes Stop for a directory demotion that ends an admin’s derived access', async () => { + const ownerId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + const directory = await seedDirectory(organizationId, { role: 'admin' }) + const demoted = await seedDirectoryMember(directory.connectionId, organizationId) + await directory.setGroupMembers([demoted.scimUserId]) + const derived = await seedRun({ userId: demoted.userId, workspaceId }) + + await directory.setGroupMembers([]) + + await expect.poll(pushedStops).toEqual([derived.streamId]) + expect(await runControl(derived)).toEqual(STOPPED) + }) + + it('pushes no Stop when the directory group write that stopped the run rolls back', async () => { + const ownerId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + const directory = await seedDirectory(organizationId, { workspaceId }) + const [removed, added] = await seedDirectoryMembersInOrder( + directory.connectionId, + organizationId, + 2 + ) + await directory.setGroupMembers([removed.scimUserId]) + const run = await seedRun({ userId: removed.userId, workspaceId }) + const allowGrants = await refuseGrantsFor(added.scimUserId) + + try { + /** The removal is projected first and stops the run; the addition then fails the write. */ + await expect(directory.setGroupMembers([added.scimUserId])).rejects.toMatchObject(REFUSED) + } finally { + await allowGrants() + } + + expect(requestedStops()).toEqual([]) + expect(await hasStop(run)).toBe(false) + expect(await runControl(run)).toEqual(LIVE) + }) + + it('pushes Stop for a committed batch when a later batch rolls back, and none for that batch', async () => { + const ownerId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + const directory = await seedDirectory(organizationId, { workspaceId }) + const members = await seedDirectoryMembersInOrder( + directory.connectionId, + organizationId, + PROJECTION_BATCH_SIZE + 2 + ) + const committed = members[0] + const rolledBack = members[PROJECTION_BATCH_SIZE] + const refused = members[PROJECTION_BATCH_SIZE + 1] + await directory.setGroupMembers([committed.scimUserId, rolledBack.scimUserId]) + const committedRun = await seedRun({ userId: committed.userId, workspaceId }) + const rolledBackRun = await seedRun({ userId: rolledBack.userId, workspaceId }) + /** Membership drifts behind the projection's back, as the scheduled pass repairs it. */ + await db.delete(scimGroupMember).where(eq(scimGroupMember.groupId, directory.groupId)) + await db + .insert(scimGroupMember) + .values({ id: generateId(), groupId: directory.groupId, scimUserId: refused.scimUserId }) + const allowGrants = await refuseGrantsFor(refused.scimUserId) + + try { + await expect( + directory.reconcile(members.map((entry) => entry.scimUserId)) + ).rejects.toMatchObject(REFUSED) + } finally { + await allowGrants() + } + + expect(requestedStops()).toEqual([committedRun.streamId]) + await expect.poll(pushedStops).toEqual([committedRun.streamId]) + expect(await runControl(committedRun)).toEqual(STOPPED) + expect(await hasStop(rolledBackRun)).toBe(false) + expect(await runControl(rolledBackRun)).toEqual(LIVE) + }) + + it('keeps a demoted admin’s run when the same pass grants its workspace', async () => { + const ownerId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + const directory = await seedDirectory(organizationId, { role: 'admin' }) + const demoted = await seedDirectoryMember(directory.connectionId, organizationId) + await directory.setGroupMembers([demoted.scimUserId]) + const run = await seedRun({ userId: demoted.userId, workspaceId }) + + await directory.remap({ workspaceId }) + await directory.reconcile([demoted.scimUserId]) + + expect(requestedStops()).toEqual([]) + expect(await hasStop(run)).toBe(false) + expect(await runControl(run)).toEqual(LIVE) + }) + + it('keeps a run when the same pass trades its workspace grant for the admin role', async () => { + const ownerId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + const directory = await seedDirectory(organizationId, { workspaceId }) + const promoted = await seedDirectoryMember(directory.connectionId, organizationId) + await directory.setGroupMembers([promoted.scimUserId]) + const run = await seedRun({ userId: promoted.userId, workspaceId }) + + await directory.remap({ role: 'admin' }) + await directory.reconcile([promoted.scimUserId]) + + expect(requestedStops()).toEqual([]) + expect(await hasStop(run)).toBe(false) + expect(await runControl(run)).toEqual(LIVE) + }) + + it('stops a removed organization member’s Assistant runs', async () => { + const ownerId = await seedUser() + const memberId = await seedUser() + const organizationId = generateId() + organizationIds.push(organizationId) + await db + .insert(organization) + .values({ id: organizationId, name: 'Run removal fixture', slug: organizationId }) + await db.insert(member).values([ + { id: generateId(), organizationId, userId: ownerId, role: 'owner' }, + { id: generateId(), organizationId, userId: memberId, role: 'member' }, + ]) + const removedRun = await seedRun({ userId: memberId, organizationId }) + const ownerRun = await seedRun({ userId: ownerId, organizationId }) + + await removeOrganizationMemberRecord({ organizationId, userId: memberId, actorUserId: ownerId }) + + await expect.poll(pushedStops).toEqual([removedRun.streamId]) + expect(await runControl(removedRun)).toEqual(STOPPED) + expect(await runControl(ownerRun)).toEqual(LIVE) + }) + + it('stops an external collaborator’s runs in the organization’s workspaces only', async () => { + const ownerId = await seedUser() + const externalId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + await grantWrite(externalId, workspaceId) + const removedRun = await seedRun({ userId: externalId, workspaceId }) + const elsewhere = await seedRun({ + userId: externalId, + workspaceId: await seedWorkspace(externalId), + }) + + await removeOrganizationMemberRecord({ + organizationId, + userId: externalId, + actorUserId: ownerId, + }) + + await expect.poll(pushedStops).toEqual([removedRun.streamId]) + expect(await runControl(removedRun)).toEqual(STOPPED) + expect(await runControl(elsewhere)).toEqual(LIVE) + }) + + it('delivers the Stop it recorded even when there was no access left to remove', async () => { + const ownerId = await seedUser() + const externalId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + const run = await seedRun({ userId: externalId, workspaceId }) + + await expect( + removeOrganizationMemberRecord({ organizationId, userId: externalId, actorUserId: ownerId }) + ).rejects.toThrow('External workspace member not found') + + await expect.poll(pushedStops).toEqual([run.streamId]) + }) + + it('delivers each Stop once when leaving a workspace also ends the organization membership', async () => { + const ownerId = await seedUser() + const memberId = await seedUser() + const { organizationId, workspaceId } = await seedOrganization(ownerId) + await db + .insert(member) + .values({ id: generateId(), organizationId, userId: memberId, role: 'member' }) + await grantWrite(memberId, workspaceId) + const workspaceRun = await seedRun({ userId: memberId, workspaceId }) + const assistantRun = await seedRun({ userId: memberId, organizationId }) + /** A live controller keeps the run unsettled, as it would be while both removals commit. */ + await getRedisClient()?.set( + chatStreamLockKey(workspaceRun.chatId), + workspaceRun.streamId, + 'EX', + 60 + ) + + await removeWorkspaceMemberRecord(workspaceId, memberId, ownerId) + + const expected = [workspaceRun.streamId, assistantRun.streamId].sort() + await expect.poll(pushedStops).toEqual(expected) + await sleep(300) + expect(pushedStops()).toEqual(expected) + await getRedisClient()?.del(chatStreamLockKey(workspaceRun.chatId)) + }) + + it('answers Stop for a suspended owner’s run', async () => { + const ownerId = await seedUser() + const run = await seedRun({ userId: ownerId, workspaceId: await seedWorkspace(ownerId) }) + + await db.update(user).set({ banned: true }).where(eq(user.id, ownerId)) + + expect(await runControl(run)).toEqual(STOPPED) + }) + + it('lets a blocked owner Stop their run, and neither steer nor send, nor anyone else Stop it', async () => { + const ownerId = await seedUser() + const memberId = await seedUser() + const workspaceId = await seedWorkspace(ownerId, [memberId]) + const run = await seedRun({ userId: ownerId, workspaceId }) + await db.update(user).set({ banned: true }).where(eq(user.id, ownerId)) + const owner = createSessionPrincipal({ userId: ownerId, sessionId: generateId() }) + const input = { streamId: run.streamId, chatId: run.chatId, workspaceId } + + await expect( + steerRun.execute({ + principal: owner, + input: { ...input, steeringId: generateId(), content: 'Keep going' }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + const sendStreamId = generateId() + expect(await acquirePendingChatStream(run.chatId, sendStreamId, 0)).toBe(true) + const lease = getLocalChatStreamLease(run.chatId, sendStreamId) + if (!lease) throw new Error('The send needs its chat lease') + const sendClaim = { normalizedKey: `run-removal:${generateId()}`, claimToken: generateId() } + await db + .insert(idempotencyKey) + .values({ key: sendClaim.normalizedKey, result: { claimToken: sendClaim.claimToken } }) + await expect( + admitChatTurn + .execute({ + principal: owner, + input: { + chatId: run.chatId, + runId: generateId(), + executionId: generateId(), + requestId: generateId(), + message: { id: sendStreamId, content: 'Start over' }, + recovery: { + kind: 'interactive_stream', + goRoute: '/api/mothership', + clientToolPickupExpected: false, + request: { + userId: ownerId, + chatId: run.chatId, + messageId: sendStreamId, + workspaceId, + message: 'Start over', + }, + }, + lease, + sendClaim, + notifyWorkspaceStatus: false, + }, + }) + .finally(() => releasePendingChatStream(run.chatId, sendStreamId, lease)) + ).rejects.toMatchObject({ code: 'forbidden' }) + await expect( + abortRun.execute({ + principal: createSessionPrincipal({ userId: memberId, sessionId: generateId() }), + input, + }) + ).rejects.toMatchObject({ code: 'not_found' }) + expect(await hasStop(run)).toBe(false) + expect(pushedStops()).toEqual([]) + + await expect(abortRun.execute({ principal: owner, input })).resolves.toMatchObject({ + aborted: true, + }) + expect(await hasStop(run)).toBe(true) + expect(pushedStops()).toEqual([run.streamId]) + }) + + /** + * Admission and deletion both write the chat row, so they serialize on its lock; holding it + * queues them in a chosen order, then releases them together. + */ + it.each(['admission', 'deletion'] as const)( + 'never leaves a run admitted while its chat is being deleted, when %s reaches the chat first', + async (first) => { + const ownerId = await seedUser() + const workspaceId = await seedWorkspace(ownerId) + const chatId = await seedChat(ownerId, workspaceId) + const streamId = generateId() + testStreams.add(streamId) + const sendClaim = { normalizedKey: `run-removal:${generateId()}`, claimToken: generateId() } + await db + .insert(idempotencyKey) + .values({ key: sendClaim.normalizedKey, result: { claimToken: sendClaim.claimToken } }) + expect(await acquirePendingChatStream(chatId, streamId, 0)).toBe(true) + const lease = getLocalChatStreamLease(chatId, streamId) + if (!lease) throw new Error('The admission needs its chat lease') + const content = 'Summarize the quarter' + const admit = () => + admitChatTurn + .execute({ + principal: createSessionPrincipal({ userId: ownerId, sessionId: generateId() }), + input: { + chatId, + runId: generateId(), + executionId: generateId(), + requestId: generateId(), + message: { id: streamId, content }, + recovery: { + kind: 'interactive_stream', + goRoute: '/api/mothership', + clientToolPickupExpected: false, + request: { + userId: ownerId, + chatId, + messageId: streamId, + workspaceId, + message: content, + }, + }, + lease, + sendClaim, + notifyWorkspaceStatus: false, + }, + }) + .then( + () => true, + (error) => { + if (!(error instanceof OrchestrationError) || error.code !== 'not_found') throw error + return false + } + ) + .finally(() => releasePendingChatStream(chatId, streamId, lease)) + const remove = () => softDeleteMothershipChat({ chatId, userId: ownerId }) + + const { admission, deletion } = await queueOnChat(chatId, first, admit, remove) + + const admitted = await admission + expect(await deletion).not.toBeNull() + const run = { userId: ownerId, chatId, streamId, runId: '', workspaceId } + expect(admitted).toBe(first === 'admission') + if (admitted) { + await expect.poll(pushedStops).toEqual([streamId]) + expect(await hasStop(run)).toBe(true) + } + expect(await runControl(run)).toEqual(STOPPED) + } + ) + + it('admits a headless run of a live chat as live', async () => { + const ownerId = await seedUser() + const workspaceId = await seedWorkspace(ownerId) + + const { run, seeded } = await admitHeadlessRun( + ownerId, + await seedChat(ownerId, workspaceId), + workspaceId + ) + + expect(run).toMatchObject({ status: 'active', toolAdmissionClosedAt: null, completedAt: null }) + expect(await runControl(seeded)).toEqual(LIVE) + }) + + it('admits a headless run of a chat deleted before it as already stopped', async () => { + const ownerId = await seedUser() + const workspaceId = await seedWorkspace(ownerId) + const chatId = await seedChat(ownerId, workspaceId) + expect(await softDeleteMothershipChat({ chatId, userId: ownerId })).not.toBeNull() + + const { run, seeded } = await admitHeadlessRun(ownerId, chatId, workspaceId) + + expect(run.status).toBe('cancelled') + expect(run.toolAdmissionClosedAt).not.toBeNull() + expect(run.completedAt).not.toBeNull() + expect(await runControl(seeded)).toEqual(STOPPED) + }) + + /** + * A headless run is admitted without writing the chat row, so only its read of the chat + * serializes it with a deletion: holding the row queues both in a chosen order. + */ + it.each(['admission', 'deletion'] as const)( + 'never leaves a headless run live while its chat is being deleted, when %s reaches the chat first', + async (first) => { + const ownerId = await seedUser() + const workspaceId = await seedWorkspace(ownerId) + const chatId = await seedChat(ownerId, workspaceId) + const admit = () => admitHeadlessRun(ownerId, chatId, workspaceId) + const remove = () => softDeleteMothershipChat({ chatId, userId: ownerId }) + + const { admission, deletion } = await queueOnChat(chatId, first, admit, remove) + + const { run, seeded } = await admission + expect(await deletion).not.toBeNull() + if (first === 'admission') { + await expect.poll(pushedStops).toEqual([seeded.streamId]) + expect(await hasStop(seeded)).toBe(true) + } else { + expect(run.status).toBe('cancelled') + expect(pushedStops()).toEqual([]) + } + expect(await toolAdmissionClosedAt(seeded)).not.toBeNull() + expect(await runControl(seeded)).toEqual(STOPPED) + } + ) +}) diff --git a/apps/sim/lib/mothership/request/application/stop-delivery.ts b/apps/sim/lib/mothership/request/application/stop-delivery.ts new file mode 100644 index 00000000000..38a38d5603e --- /dev/null +++ b/apps/sim/lib/mothership/request/application/stop-delivery.ts @@ -0,0 +1,120 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { ringDesktopInbox } from '@/lib/desktop/executor/doorbell' +import { markExecutionCancelled } from '@/lib/execution/cancellation' +import { abortManualExecution } from '@/lib/execution/manual-cancellation' +import { settleStoppedRunWithoutController } from '@/lib/mothership/async-runs/orphaned-runs' +import type { + RunStopTarget, + StoppedRun, + StoppedRunWork, +} from '@/lib/mothership/async-runs/repository' +import { + abortActiveStream, + releasePendingChatStream, + waitForPendingChatStream, +} from '@/lib/mothership/request/session' +import { requestExplicitStreamAbort } from '@/lib/mothership/request/session/explicit-abort' +import { recoverSandboxProcesses } from '@/lib/mothership/request/tools/sandbox-recovery' + +const logger = createLogger('MothershipStopDelivery') + +interface RunStopDelivery { + /** The run's controller let go of its chat; otherwise its lock was force-released. */ + settled: boolean + /** The worker acknowledged the Stop as settled. */ + workerSettled: boolean +} + +interface RunStopDeliveryOptions { + /** The worker endpoint, resolved while the run's user still exists. */ + mothershipBaseURL?: string +} + +/** + * Delivers a Stop that is already durable to everything still acting on the run: the + * desktop, the worker, this process's controller, the run's client workflows and sandbox + * commands. The signals go out first; `work` may still be loading. Each effect is best-effort; the worker also reconciles Sim's durable intent on + * its next heartbeat. + */ +export async function deliverRunStop( + run: RunStopTarget, + work: StoppedRunWork | Promise, + options: RunStopDeliveryOptions = {} +): Promise { + const { streamId, userId, chatId } = run + /** Admission is closed: the device re-reads its inbox and cancels what it is running. */ + if (run.desktopDeviceId) ringDesktopInbox(run.desktopDeviceId, 'cancel') + /** Push delivers promptly; the worker also reconciles Sim's durable intent after an outage. */ + const workerStop = requestExplicitStreamAbort({ + streamId, + userId, + chatId, + timeoutMs: 6000, + mothershipBaseURL: options.mothershipBaseURL, + }).catch((error) => { + logger.warn('Stop saved; worker delivery awaits reconciliation', { + streamId, + error: getErrorMessage(error), + }) + return { settled: false } + }) + /** The Stop is durable already; a lost abort marker must not skip the cleanup below. */ + try { + await abortActiveStream(streamId) + } catch (error) { + logger.warn('Stopped stream abort marker could not be written', { + streamId, + error: getErrorMessage(error), + }) + } + const cancelClientWorkflows = async () => + Promise.all( + (await work).clientWorkflowExecutionIds.map(async (executionId) => { + /** The execution route reserved these IDs before recording ownership; no client result is trusted. */ + abortManualExecution(executionId) + await markExecutionCancelled(executionId) + }) + ) + const [settled, worker] = await Promise.all([ + waitForPendingChatStream(chatId, 8000, streamId), + workerStop, + Promise.resolve(work).then(({ sandboxProcesses }) => + recoverSandboxProcesses(sandboxProcesses, AbortSignal.timeout(8000)) + ), + cancelClientWorkflows().catch((error) => { + logger.warn('Stopped stream workflow cancellation could not be delivered', { + streamId, + error: getErrorMessage(error), + }) + }), + ]) + if (!settled) { + await releasePendingChatStream(chatId, streamId) + logger.warn('Stopped stream did not settle; released its chat lock', { chatId, streamId }) + } + /** A run with no controller left, or none to begin with, has nothing else to settle it. */ + await settleStoppedRunWithoutController(run.id).catch((error) => { + logger.warn('Stopped run without a controller could not be settled', { + streamId, + error: getErrorMessage(error), + }) + }) + return { settled, workerSettled: worker.settled } +} + +/** + * Starts delivering the Stop a deletion or access removal committed for each of its live + * runs, without holding up the removal: a controller can take seconds to let go, and run + * control already answers Stop for every run the removal ended. Never throws. + */ +export function deliverRunStops(runs: StoppedRun[], options: RunStopDeliveryOptions = {}): void { + for (const run of runs) { + deliverRunStop(run, run, options).catch((error) => + logger.warn('Stop for a removed run was not delivered', { + streamId: run.streamId, + error: getErrorMessage(error), + }) + ) + } +} diff --git a/apps/sim/lib/mothership/request/handlers/handlers.test.ts b/apps/sim/lib/mothership/request/handlers/handlers.test.ts index bb1c1e63993..358d6e4cc9c 100644 --- a/apps/sim/lib/mothership/request/handlers/handlers.test.ts +++ b/apps/sim/lib/mothership/request/handlers/handlers.test.ts @@ -66,6 +66,7 @@ import { MothershipStreamV1ToolOutcome, MothershipStreamV1ToolPhase, } from '@/lib/mothership/generated/mothership-stream-v1' +import { TOOL_DEADLINE_SETTLE_MARGIN_MS } from '@/lib/mothership/generated/protocol' import { Read as ReadTool, RunFunction } from '@/lib/mothership/generated/tool-catalog-v1' import { applyStreamEvent, @@ -75,6 +76,10 @@ import { } from '@/lib/mothership/request/handlers' import { createEvent } from '@/lib/mothership/request/session/event' import { shouldSkipToolCallEvent } from '@/lib/mothership/request/sse-utils' +import { + TOOL_DEADLINE_BEFORE_START_MESSAGE, + TOOL_DEADLINE_EXCEEDED_MESSAGE, +} from '@/lib/mothership/request/tools/deadline' import type { ExecutionContext, StreamEvent, @@ -661,6 +666,203 @@ describe('sse-handlers tool lifecycle', () => { expect(context.finalAssistantContent).toBe('Final answer only.') }) + describe('the deadline a frame names', () => { + /** A call frame naming `deadlineMs`; `client` makes it one a browser or desktop runs. */ + const deadlineFrame = ( + deadlineMs: number, + call: { toolCallId?: string; toolName?: string; arguments?: Record } = {}, + client = false + ): StreamEvent => ({ + type: MothershipStreamV1EventType.tool, + payload: { + toolCallId: call.toolCallId ?? 'audio-1', + toolName: call.toolName ?? 'generate_audio', + arguments: call.arguments ?? { prompt: 'p' }, + executor: client + ? MothershipStreamV1ToolExecutor.client + : MothershipStreamV1ToolExecutor.sim, + mode: MothershipStreamV1ToolMode.async, + phase: MothershipStreamV1ToolPhase.call, + ...(client ? {} : { ui: { simExecutable: true } }), + deadlineMs, + }, + }) + + it('bounds the call from its receipt, less the settle margin', async () => { + executeTool.mockResolvedValueOnce({ success: true, output: {} }) + const before = Date.now() + await sseHandlers.tool(deadlineFrame(900_000), context, execContext, { interactive: false }) + const after = Date.now() + const deadlineAt = context.toolCalls.get('audio-1')?.deadlineAt ?? 0 + expect(deadlineAt).toBeGreaterThanOrEqual(before + 900_000 - TOOL_DEADLINE_SETTLE_MARGIN_MS) + expect(deadlineAt).toBeLessThanOrEqual(after + 900_000 - TOOL_DEADLINE_SETTLE_MARGIN_MS) + }) + + it('gives a client call only what remains of the deadline recorded at first hand-off', async () => { + const recorded = new Date(Date.now() + 60_000) + mothershipAsyncRunsMockFns.mockGetAsyncToolCall.mockResolvedValueOnce({ + executionDeadlineAt: recorded, + }) + await sseHandlers.tool( + deadlineFrame(900_000, { toolName: 'browser_click', arguments: {} }, true), + context, + execContext, + { interactive: true } + ) + expect(context.toolCalls.get('audio-1')?.deadlineAt).toBe(recorded.getTime()) + }) + + it('counts the deadline as spent when the recorded one cannot be read', async () => { + mothershipAsyncRunsMockFns.mockGetAsyncToolCall.mockRejectedValueOnce(new Error('db down')) + const before = Date.now() + await sseHandlers.tool( + deadlineFrame( + 900_000, + { toolCallId: 'takeover-1', toolName: 'browser_click', arguments: {} }, + true + ), + context, + execContext, + { interactive: true } + ) + expect(context.toolCalls.get('takeover-1')?.deadlineAt).toBeLessThan(before + 60_000) + }) + + /** Runs a desktop-local read whose frame names `deadlineMs`, recording the emitted events. */ + async function runLocalRead(deadlineMs: number) { + const onEvent = vi.fn() + await sseHandlers.tool( + deadlineFrame( + deadlineMs, + { + toolCallId: 'local-read-1', + toolName: 'read', + arguments: { path: 'user-local/Project--mount/README.md' }, + }, + true + ), + context, + execContext, + { onEvent, interactive: true } + ) + return onEvent + } + + /** The call ended with `message`, and the client was sent a result naming it. */ + function expectFailedWith(onEvent: ReturnType, message: string) { + expect(context.toolCalls.get('local-read-1')).toMatchObject({ + status: MothershipStreamV1ToolOutcome.error, + error: message, + }) + expect(onEvent).toHaveBeenCalledWith( + expect.objectContaining({ + payload: expect.objectContaining({ + phase: MothershipStreamV1ToolPhase.result, + error: message, + }), + }) + ) + } + + it('never offers an unclaimed client call whose deadline already passed', async () => { + mothershipAsyncRunsMockFns.mockCompletePendingAsyncToolCall.mockImplementationOnce( + async (input) => ({ ...input }) + ) + const onEvent = await runLocalRead(TOOL_DEADLINE_SETTLE_MARGIN_MS) + await Promise.allSettled(context.pendingToolPromises.values()) + + expect(waitForClientToolCompletion).not.toHaveBeenCalled() + expectFailedWith(onEvent, TOOL_DEADLINE_BEFORE_START_MESSAGE) + }) + + it('does not offer an unclaimed client call with less than a minute of its deadline left', async () => { + mothershipAsyncRunsMockFns.mockCompletePendingAsyncToolCall.mockImplementationOnce( + async (input) => ({ ...input }) + ) + const onEvent = await runLocalRead(TOOL_DEADLINE_SETTLE_MARGIN_MS + 30_000) + await Promise.allSettled(context.pendingToolPromises.values()) + + expect(waitForClientToolCompletion).not.toHaveBeenCalled() + expectFailedWith(onEvent, TOOL_DEADLINE_BEFORE_START_MESSAGE) + }) + + it('settles a client call nothing claimed by its deadline as not run', async () => { + vi.useFakeTimers() + try { + waitForClientToolCompletion.mockImplementationOnce( + () => new Promise((resolve) => setTimeout(() => resolve(null), 65_000)) + ) + mothershipAsyncRunsMockFns.mockCompletePendingAsyncToolCall.mockImplementationOnce( + async (input) => ({ ...input }) + ) + const onEvent = await runLocalRead(TOOL_DEADLINE_SETTLE_MARGIN_MS + 65_000) + await vi.advanceTimersByTimeAsync(65_000) + await Promise.allSettled(context.pendingToolPromises.values()) + + expect(waitForClientToolCompletion).toHaveBeenCalledOnce() + expectFailedWith(onEvent, TOOL_DEADLINE_BEFORE_START_MESSAGE) + } finally { + vi.useRealTimers() + } + }) + + it('reads a claimed call past its deadline and reports its outcome as unknown', async () => { + const onEvent = await runLocalRead(TOOL_DEADLINE_SETTLE_MARGIN_MS) + await Promise.allSettled(context.pendingToolPromises.values()) + + expect(waitForClientToolCompletion).toHaveBeenCalledOnce() + expectFailedWith(onEvent, TOOL_DEADLINE_EXCEEDED_MESSAGE) + }) + + it('counts the deadline as spent when its lookup outlasts it', async () => { + vi.useFakeTimers() + try { + mothershipAsyncRunsMockFns.mockGetAsyncToolCall.mockImplementationOnce( + () => new Promise(() => {}) + ) + const handled = sseHandlers.tool( + deadlineFrame( + TOOL_DEADLINE_SETTLE_MARGIN_MS + 2_000, + { toolCallId: 'stalled-1', toolName: 'browser_click', arguments: {} }, + true + ), + context, + execContext, + { interactive: true } + ) + await vi.advanceTimersByTimeAsync(2_000) + await handled + + expect(context.toolCalls.get('stalled-1')?.deadlineAt).toBeLessThanOrEqual(Date.now()) + } finally { + vi.useRealTimers() + } + }) + + it('fails a client call still unfinished at its deadline', async () => { + vi.useFakeTimers() + try { + // The waiter gives up when its (deadline-capped) timeout ends, five seconds in. + waitForClientToolCompletion.mockImplementationOnce( + () => new Promise((resolve) => setTimeout(() => resolve(null), 5_000)) + ) + const onEvent = await runLocalRead(TOOL_DEADLINE_SETTLE_MARGIN_MS + 5_000) + await vi.advanceTimersByTimeAsync(5_000) + await Promise.allSettled(context.pendingToolPromises.values()) + + expectFailedWith(onEvent, TOOL_DEADLINE_EXCEEDED_MESSAGE) + } finally { + vi.useRealTimers() + } + }) + + it('leaves a call without one on Sim\u2019s own bounds', async () => { + executeTool.mockResolvedValueOnce({ success: true, output: {} }) + await sseHandlers.tool(deadlineFrame(-1), context, execContext, { interactive: false }) + expect(context.toolCalls.get('audio-1')?.deadlineAt).toBeUndefined() + }) + }) + it('executes a display-named frame by its execName (the worker CLI class)', async () => { // The worker wires sim_cli frames with toolName cli_* (display identity) and // execName sim_cli. Dispatching by the display name sent every CLI call into diff --git a/apps/sim/lib/mothership/request/handlers/tool.ts b/apps/sim/lib/mothership/request/handlers/tool.ts index 6f9e3757af3..533cee21dc5 100644 --- a/apps/sim/lib/mothership/request/handlers/tool.ts +++ b/apps/sim/lib/mothership/request/handlers/tool.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' +import { withinDeadline } from '@/lib/core/utils/deadline' import { ringDesktopInbox } from '@/lib/desktop/executor/doorbell' import { getRunDesktopDeviceId } from '@/lib/desktop/executor/repository' import { AsyncToolCallOwnershipError } from '@/lib/mothership/async-runs/errors' @@ -7,7 +8,7 @@ import type { AsyncCompletionSignal, AsyncTerminalCompletionSnapshot, } from '@/lib/mothership/async-runs/lifecycle' -import { upsertAsyncToolCall } from '@/lib/mothership/async-runs/repository' +import { getAsyncToolCall, upsertAsyncToolCall } from '@/lib/mothership/async-runs/repository' import { CLIENT_TOOL_RESULT_TIMEOUT_MS, COPILOT_WORKFLOW_TOOL_CLIENT_GRACE_MS, @@ -33,8 +34,21 @@ import { markToolResultSeen, wasToolResultSeen } from '@/lib/mothership/request/ import { setTerminalToolCallState } from '@/lib/mothership/request/tool-call-state' import { waitForClientToolCompletion } from '@/lib/mothership/request/tools/client' import { sealClientToolContext } from '@/lib/mothership/request/tools/client-completion-seal.server' +import { + capToToolDeadline, + frameToolDeadlineAt, + TOOL_DEADLINE_BEFORE_START_MESSAGE, + TOOL_DEADLINE_EXCEEDED_MESSAGE, + toDeadlineDate, + toolDeadlinePassed, + toolStartWindowClosed, +} from '@/lib/mothership/request/tools/deadline' import { waitForDesktopToolCall } from '@/lib/mothership/request/tools/desktop-wait' -import { executeToolAndReport } from '@/lib/mothership/request/tools/executor' +import { + executeToolAndReport, + failPendingToolCall, + failUnstartedToolCall, +} from '@/lib/mothership/request/tools/executor' import { runGatedToolExecution, TOOL_AWAITING_APPROVAL_STATUS, @@ -351,6 +365,7 @@ export async function prePersistClientExecutableToolCall( ? MothershipStreamV1AsyncToolRecordStatus.pending : MothershipStreamV1AsyncToolRecordStatus.running, permissionRequested: gated, + executionDeadlineAt: toDeadlineDate(frameToolDeadlineAt(data.deadlineMs, Date.now())), }).catch((err) => { if (err instanceof AsyncToolCallOwnershipError) throw err logger.warn('Failed to pre-persist async tool row before forwarding call frame', { @@ -500,6 +515,48 @@ function stampToolCallBlockEnd( } } +/** How long a client wait lasts at least: enough to read a result the client already saved. */ +const STORED_RESULT_READ_MS = 5_000 + +/** + * The completion a client wait reports for a call Sim settled itself: whatever the settlement + * left on the call, which is the desktop's own result when that landed first. + */ +function settledClientCompletion(toolCall: ToolCallState): AsyncTerminalCompletionSnapshot { + const status = + toolCall.status === MothershipStreamV1ToolOutcome.success || + toolCall.status === MothershipStreamV1ToolOutcome.cancelled + ? toolCall.status + : MothershipStreamV1ToolOutcome.error + return { + status, + ...(toolCall.error ? { message: toolCall.error } : {}), + ...(toolCall.result?.output !== undefined ? { data: toolCall.result.output } : {}), + } +} + +/** + * Adopts the deadline recorded when the call was first handed (a takeover re-hands its parked + * calls), so an approval or client wait gets only what remains of it. Server executions adopt it + * from their own upsert. A recorded deadline that cannot be read before the frame's runs out + * counts as spent: the frame's could be later than the one the worker is holding the call to. + */ +async function adoptRecordedToolDeadline(toolCall: ToolCallState): Promise { + if (toolCall.deadlineAt === undefined) return + try { + const recorded = ( + await withinDeadline(() => getAsyncToolCall(toolCall.id), toolCall.deadlineAt) + )?.executionDeadlineAt + if (recorded) toolCall.deadlineAt = recorded.getTime() + } catch (error) { + logger.warn('Could not read a tool call deadline; treating it as spent', { + toolCallId: toolCall.id, + error: getErrorMessage(error), + }) + toolCall.deadlineAt = Date.now() + } +} + async function handleCallPhase( data: ToolCallStreamEvent['payload'], context: StreamingContext, @@ -610,6 +667,7 @@ async function handleCallPhase( // apply_file_edit intent handoff to one file subagent under concurrency. if (parentToolCallId) toolCall.parentToolCallId = parentToolCallId if (data.execName) toolCall.execName = data.execName + toolCall.deadlineAt ??= frameToolDeadlineAt(data.deadlineMs, Date.now()) const readPath = typeof args?.path === 'string' ? args.path : undefined if (toolName === 'read' && readPath?.startsWith('internal/')) return @@ -888,7 +946,9 @@ async function dispatchToolExecution( return fireToolExecution() } - if (toolCallNeedsApproval(toolName, context, options, frameRequestsApproval, args)) { + const gated = toolCallNeedsApproval(toolName, context, options, frameRequestsApproval, args) + if (gated || clientExecutable) await adoptRecordedToolDeadline(toolCall) + if (gated) { registerPendingToolPromise( context, toolCallId, @@ -918,7 +978,12 @@ async function dispatchToolExecution( */ function waitForClientExecution(): Promise { toolCall.status = 'executing' - const timeoutMs = options.timeout || CLIENT_TOOL_RESULT_TIMEOUT_MS + const ownTimeoutMs = options.timeout || CLIENT_TOOL_RESULT_TIMEOUT_MS + // Even past its deadline a wait lasts long enough to read a result the client already saved. + const timeoutMs = Math.max( + capToToolDeadline(ownTimeoutMs, toolCall.deadlineAt, Date.now()), + Math.min(ownTimeoutMs, STORED_RESULT_READ_MS) + ) return withCopilotSpan( TraceSpan.CopilotToolWaitForClientResult, { @@ -932,7 +997,20 @@ async function dispatchToolExecution( const desktopDeviceId = isBackgroundDesktopToolCall(toolName, args) ? await resolveRunDesktopDevice(context) : null - if (isWorkflowToolName(toolName)) { + // An approval wait or a spent recorded deadline left no time. Only a call nothing claimed + // provably never ran; a claimed one (a takeover re-hands calls) is read, then settled + // below as outcome unknown. + if ( + toolStartWindowClosed(toolCall.deadlineAt, Date.now()) && + (await failUnstartedToolCall( + toolCallId, + context, + execContext, + TOOL_DEADLINE_BEFORE_START_MESSAGE + )) + ) { + completion = settledClientCompletion(toolCall) + } else if (isWorkflowToolName(toolName)) { const race = await raceWorkflowToolClientPickup({ toolCallId, select: @@ -943,6 +1021,7 @@ async function dispatchToolExecution( : undefined, workflowId: resolveWorkflowToolTargetId(args, execContext.workflowId), timeoutMs, + deadlineAt: toolCall.deadlineAt, // The caller declared its executors at turn setup (ChatRequest.clientCapabilities): // no pickup capability → zero grace, the race resolves straight to the server // claim. Same claim arbitration either way — the grace only encodes how long a @@ -1000,6 +1079,25 @@ async function dispatchToolExecution( }) } span.setAttribute(TraceAttr.ToolExecutor, MothershipStreamV1ToolExecutor.client) + if (completion === null && toolDeadlinePassed(toolCall.deadlineAt, Date.now())) { + // A call nothing claimed never ran. A claimed one is settled durably as outcome + // unknown, so a desktop that claimed it stops renewing it. + if ( + !(await failUnstartedToolCall( + toolCallId, + context, + execContext, + TOOL_DEADLINE_BEFORE_START_MESSAGE + )) + ) + await failPendingToolCall( + toolCallId, + context, + execContext, + TOOL_DEADLINE_EXCEEDED_MESSAGE + ) + completion = settledClientCompletion(toolCall) + } // Both waiters resolve `T | null`, never undefined — comparing against // undefined made this a constant `true` and hid every timeout. span.setAttribute(TraceAttr.ToolCompletionReceived, completion !== null) diff --git a/apps/sim/lib/mothership/request/lifecycle/controller-ownership.ts b/apps/sim/lib/mothership/request/lifecycle/controller-ownership.ts index a00816b74ed..1ff87d0a524 100644 --- a/apps/sim/lib/mothership/request/lifecycle/controller-ownership.ts +++ b/apps/sim/lib/mothership/request/lifecycle/controller-ownership.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { copilotChats, copilotRuns } from '@sim/db/schema' import { backoffWithJitter } from '@sim/utils/retry' -import { and, eq, notInArray, sql } from 'drizzle-orm' +import { and, eq, isNull, notInArray, sql } from 'drizzle-orm' import { z } from 'zod' /** @@ -63,11 +63,13 @@ export async function claimRunController(input: { recoveryBackoff: RecoveryBackoff }): Promise { return db.transaction(async (tx) => { - await tx + /** A chat deleted before the claim has stopped its run: no controller adopts it. */ + const [chat] = await tx .select({ id: copilotChats.id }) .from(copilotChats) - .where(eq(copilotChats.id, input.chatId)) + .where(and(eq(copilotChats.id, input.chatId), isNull(copilotChats.deletedAt))) .for('update') + if (!chat) return false const [run] = await tx .update(copilotRuns) .set({ diff --git a/apps/sim/lib/mothership/request/lifecycle/run.test.ts b/apps/sim/lib/mothership/request/lifecycle/run.test.ts index 4a7640c1071..7e4cb2a2658 100644 --- a/apps/sim/lib/mothership/request/lifecycle/run.test.ts +++ b/apps/sim/lib/mothership/request/lifecycle/run.test.ts @@ -3555,8 +3555,11 @@ describe('runCopilotLifecycle', () => { } }) - /** Runs a turn whose import never settles, recording each leg's request body. */ - function runImportTurn() { + /** + * Runs a turn whose import never settles, recording each leg's request body. `deadlineAt` is + * the deadline the worker named for the import, if any. + */ + function runImportTurn(deadlineAt?: number) { const bodies: Record[] = [] let streamContext: StreamingContext | undefined mockForceFailHungToolCall.mockImplementation( @@ -3577,6 +3580,7 @@ describe('runCopilotLifecycle', () => { id: 'tool-import', name: 'import_local_files', status: 'executing', + ...(deadlineAt === undefined ? {} : { deadlineAt }), }) context.pendingToolPromises.set('tool-import', new Promise(() => {})) context.awaitingAsyncContinuation = { @@ -3648,6 +3652,61 @@ describe('runCopilotLifecycle', () => { } }) + it('fails a renewed import at the deadline the worker named for it', async () => { + vi.useFakeTimers() + try { + mothershipAsyncRunsMockFns.mockGetChatViewDesktopLeaseRemainingMs.mockResolvedValue(50_000) + const turn = runImportTurn(Date.now() + 200_000) + // Its lease is renewed throughout, so only the deadline ends the wait. + await vi.advanceTimersByTimeAsync(229_000) + expect(turn.bodies).toHaveLength(1) + await vi.advanceTimersByTimeAsync(2_000) + expect((await turn.lifecycle).success).toBe(true) + expect(turn.resumedWithLostImport()).toBe(true) + expect(turn.forceFailures()).toEqual([ + 'Pending tool execution reached the client tool result cap; force-failing', + ]) + } finally { + mothershipAsyncRunsMockFns.mockGetChatViewDesktopLeaseRemainingMs.mockReset() + vi.useRealTimers() + } + }) + + it('fails a call whose deadline already passed at that deadline plus grace, not a grace from now', async () => { + vi.useFakeTimers() + try { + mothershipAsyncRunsMockFns.mockGetChatViewDesktopLeaseRemainingMs.mockResolvedValue(50_000) + const turn = runImportTurn(Date.now() - 10_000) + await vi.advanceTimersByTimeAsync(19_000) + expect(turn.bodies).toHaveLength(1) + await vi.advanceTimersByTimeAsync(2_000) + expect((await turn.lifecycle).success).toBe(true) + expect(turn.resumedWithLostImport()).toBe(true) + expect( + mothershipAsyncRunsMockFns.mockGetChatViewDesktopLeaseRemainingMs + ).not.toHaveBeenCalled() + } finally { + mothershipAsyncRunsMockFns.mockGetChatViewDesktopLeaseRemainingMs.mockReset() + vi.useRealTimers() + } + }) + + it('a call without a deadline keeps its full budget and grace', async () => { + vi.useFakeTimers() + try { + mothershipAsyncRunsMockFns.mockGetChatViewDesktopLeaseRemainingMs.mockResolvedValue(null) + const turn = runImportTurn() + await vi.advanceTimersByTimeAsync(89_000) + expect(turn.bodies).toHaveLength(1) + await vi.advanceTimersByTimeAsync(2_000) + expect((await turn.lifecycle).success).toBe(true) + expect(turn.resumedWithLostImport()).toBe(true) + } finally { + mothershipAsyncRunsMockFns.mockGetChatViewDesktopLeaseRemainingMs.mockReset() + vi.useRealTimers() + } + }) + it('a lease lookup that fails is checked again instead of failing a renewed import', async () => { vi.useFakeTimers() try { diff --git a/apps/sim/lib/mothership/request/lifecycle/run.ts b/apps/sim/lib/mothership/request/lifecycle/run.ts index 923b4053747..df28b660535 100644 --- a/apps/sim/lib/mothership/request/lifecycle/run.ts +++ b/apps/sim/lib/mothership/request/lifecycle/run.ts @@ -1446,19 +1446,26 @@ async function runCheckpointLoop( } for (const [toolCallId, promise] of context.pendingToolPromises) { if (pendingWatchdogs.get(toolCallId)?.promise === promise) continue + const toolCall = context.toolCalls.get(toolCallId) const waitBudgetMs = - pendingToolWaitBudgetMs(context.toolCalls.get(toolCallId), context.desktopDeviceId) + + pendingToolWaitBudgetMs(toolCall, context.desktopDeviceId) + TOOL_WATCHDOG_RESUME_GRACE_MS + const clientCapAt = now + Math.max(waitBudgetMs, CLIENT_TOOL_RESULT_TIMEOUT_MS) + // Neither the wait nor a lease carries a call past the deadline the worker named for it. + const ceilingAt = + toolCall?.deadlineAt === undefined + ? clientCapAt + : Math.min(clientCapAt, toolCall.deadlineAt + TOOL_WATCHDOG_RESUME_GRACE_MS) pendingWatchdogs.set(toolCallId, { promise, settlement: promise.then( () => ({ toolCallId, promise }), () => ({ toolCallId, promise }) ), - deadlineAt: now + waitBudgetMs, + deadlineAt: Math.min(now + waitBudgetMs, ceilingAt), waitBudgetMs, startedAt: now, - ceilingAt: now + Math.max(waitBudgetMs, CLIENT_TOOL_RESULT_TIMEOUT_MS), + ceilingAt, }) maximumWaitBudgetMs = Math.max(maximumWaitBudgetMs, waitBudgetMs) } diff --git a/apps/sim/lib/mothership/request/session/abort.test.ts b/apps/sim/lib/mothership/request/session/abort.test.ts index cd007c07f18..31a797c5b29 100644 --- a/apps/sim/lib/mothership/request/session/abort.test.ts +++ b/apps/sim/lib/mothership/request/session/abort.test.ts @@ -16,12 +16,27 @@ vi.mock('@/lib/mothership/request/session/buffer', () => ({ vi.mock('@/lib/mothership/request/otel', () => mothershipOtelMock) import { + abortActiveStream, acquirePendingChatStream, getChatStreamLockOwners, + registerActiveStream, releasePendingChatStream, startAbortPoller, } from '@/lib/mothership/request/session/abort' +describe('abortActiveStream', () => { + it('stops this process’s controller even when the abort marker cannot be written', async () => { + const controller = new AbortController() + registerActiveStream('stream-marker-down', controller) + mockWriteAbortMarker.mockRejectedValueOnce(new Error('redis unavailable')) + + await expect(abortActiveStream('stream-marker-down')).rejects.toThrow('redis unavailable') + + expect(controller.signal.aborted).toBe(true) + expect(await abortActiveStream('stream-marker-down')).toBe(false) + }) +}) + describe('startAbortPoller heartbeat', () => { beforeEach(() => { vi.useFakeTimers() diff --git a/apps/sim/lib/mothership/request/session/abort.ts b/apps/sim/lib/mothership/request/session/abort.ts index 8d32e0cf03d..840b41198d8 100644 --- a/apps/sim/lib/mothership/request/session/abort.ts +++ b/apps/sim/lib/mothership/request/session/abort.ts @@ -312,17 +312,21 @@ export async function abortActiveStream(streamId: string): Promise { TraceSpan.CopilotChatAbortActiveStream, { [TraceAttr.StreamId]: streamId }, async (span) => { - await writeAbortMarker(streamId) - span.setAttribute(TraceAttr.CopilotAbortMarkerWritten, true) - const controller = activeStreams.get(streamId) - if (!controller) { - span.setAttribute(TraceAttr.CopilotAbortControllerFired, false) - return false + let fired = false + /** This process's controller stops even when the cross-process marker cannot be written. */ + try { + await writeAbortMarker(streamId) + span.setAttribute(TraceAttr.CopilotAbortMarkerWritten, true) + } finally { + const controller = activeStreams.get(streamId) + if (controller) { + controller.abort(AbortReason.UserStop) + activeStreams.delete(streamId) + fired = true + } + span.setAttribute(TraceAttr.CopilotAbortControllerFired, fired) } - controller.abort(AbortReason.UserStop) - activeStreams.delete(streamId) - span.setAttribute(TraceAttr.CopilotAbortControllerFired, true) - return true + return fired } ) } diff --git a/apps/sim/lib/mothership/request/session/contract.ts b/apps/sim/lib/mothership/request/session/contract.ts index 22348382fc3..e73a63a1c97 100644 --- a/apps/sim/lib/mothership/request/session/contract.ts +++ b/apps/sim/lib/mothership/request/session/contract.ts @@ -19,6 +19,7 @@ import type { StreamActivityCheckpoint, StreamTextCompletion, StreamTextPosition, + StreamToolDeadline, StreamToolReplay, } from '@/lib/mothership/generated/protocol' import { ResourcePayload } from '@/lib/mothership/generated/resources' @@ -46,7 +47,7 @@ type EnvelopeToStreamEvent = T extends { : TType extends 'complete' ? TPayload & StreamTextCompletion & StreamActivityCheckpoint : TType extends 'tool' - ? TPayload & StreamToolReplay + ? TPayload & StreamToolReplay & StreamToolDeadline : TType extends 'resource' ? ResourcePayload : TType extends 'run' @@ -132,7 +133,7 @@ export interface SyntheticFilePreviewEventEnvelope { type ResourceEnvelope = Extract type ToolEnvelope = Extract type ToolReplayEnvelope = T extends ToolEnvelope - ? Omit & { payload: T['payload'] & StreamToolReplay } + ? Omit & { payload: T['payload'] & StreamToolReplay & StreamToolDeadline } : never type ContractEventEnvelope = diff --git a/apps/sim/lib/mothership/request/session/replay-budget.integration.ts b/apps/sim/lib/mothership/request/session/replay-budget.integration.ts index 8c530c1aa5c..3d723c3176f 100644 --- a/apps/sim/lib/mothership/request/session/replay-budget.integration.ts +++ b/apps/sim/lib/mothership/request/session/replay-budget.integration.ts @@ -134,7 +134,7 @@ vi.mock('@/lib/mothership/request/lifecycle/run', () => ({ import { trace } from '@opentelemetry/api' import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base' import { db } from '@sim/db' -import { copilotChats, copilotRuns, permissions, user, workspace } from '@sim/db/schema' +import { copilotChats, copilotRuns, permissions, user, workflow, workspace } from '@sim/db/schema' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { NextRequest } from 'next/server' @@ -171,7 +171,6 @@ import { type PendingFileIntent, storeFileIntent, } from '@/lib/mothership/tools/server/files/intent-store' -import { GET as copilotChatGET } from '@/app/api/copilot/chat/queries' import { GET as streamGET } from '@/app/api/copilot/chat/stream/route' import { GET as mothershipChatGET } from '@/app/api/mothership/chats/[chatId]/route' @@ -986,30 +985,51 @@ describe.runIf(Boolean(redisUrl))('a turn whose stream exhausts its replay budge expect(worker.runs.map((run) => run.recoveredEvents)).toEqual([[]]) }) + const loadChat = () => + mothershipChatGET(new NextRequest(`http://localhost:3000/api/mothership/chats/${chatId}`), { + params: Promise.resolve({ chatId }), + }) + + /** Serves the chat as a workflow Copilot panel chat for the length of one read. */ + async function loadAsWorkflowCopilotChat() { + const workflowId = generateId() + const now = new Date() + await db.insert(workflow).values({ + id: workflowId, + userId, + workspaceId, + name: 'Copilot panel workflow', + lastSynced: now, + createdAt: now, + updatedAt: now, + }) + await db + .update(copilotChats) + .set({ type: 'copilot', workflowId }) + .where(eq(copilotChats.id, chatId)) + try { + return await loadChat() + } finally { + await db + .update(copilotChats) + .set({ type: 'mothership', workflowId: null }) + .where(eq(copilotChats.id, chatId)) + await db.delete(workflow).where(eq(workflow.id, workflowId)) + } + } + it.each([ - [ - 'mothership chat', - () => - mothershipChatGET( - new NextRequest(`http://localhost:3000/api/mothership/chats/${chatId}`), - { - params: Promise.resolve({ chatId }), - } - ), - ], - [ - 'copilot chat', - () => - copilotChatGET( - new NextRequest(`http://localhost:3000/api/copilot/chat?chatId=${chatId}`) - ), - ], + ['mothership chat', loadChat], + ['workflow copilot chat', loadAsWorkflowCopilotChat], ])('leaves the %s snapshot to the resume route', async (_label, load) => { const streamId = await trimmedRecoverableStream() - const body = await (await load()).json() + const response = await load() + expect(response.status).toBe(200) + const body = await response.json() expect(body.success).toBe(true) + expect(body.chat.mode).toBe('agent') expect(body.chat.streamSnapshot).toBeUndefined() expect(JSON.stringify(body.chat.messages)).not.toContain('part 3') expect(await storedMembers(streamId)).toHaveLength(2) diff --git a/apps/sim/lib/mothership/request/tools/deadline.ts b/apps/sim/lib/mothership/request/tools/deadline.ts new file mode 100644 index 00000000000..4a013afdcc8 --- /dev/null +++ b/apps/sim/lib/mothership/request/tools/deadline.ts @@ -0,0 +1,65 @@ +import { SIM_TOOL_MIN_START_BUDGET_MS } from '@/lib/mothership/async-runs/execution-lease' +import { TOOL_DEADLINE_SETTLE_MARGIN_MS } from '@/lib/mothership/generated/protocol' + +/** The longest worker wait a frame may name; a larger or malformed value is ignored. */ +const MAX_FRAME_DEADLINE_MS = 7 * 24 * 60 * 60 * 1000 + +/** + * When Sim must have settled a call whose frame names the worker's wait (`deadlineMs`, from + * hand-off), received at `receivedAt`. Undefined when the frame names none: the call keeps + * Sim's own bounds. + */ +export function frameToolDeadlineAt(deadlineMs: unknown, receivedAt: number): number | undefined { + if ( + typeof deadlineMs !== 'number' || + !Number.isSafeInteger(deadlineMs) || + deadlineMs <= 0 || + deadlineMs > MAX_FRAME_DEADLINE_MS + ) + return undefined + return receivedAt + deadlineMs - TOOL_DEADLINE_SETTLE_MARGIN_MS +} + +/** What the model learns when a call's deadline passed before the call could start. */ +export const TOOL_DEADLINE_BEFORE_START_MESSAGE = + 'This tool call ran out of its time limit before it could start, so it did not run. Nothing changed; it is safe to try again with a smaller request.' + +/** What the model learns when a call that started was still unfinished at its deadline. */ +export const TOOL_DEADLINE_EXCEEDED_MESSAGE = + 'This tool call did not finish within its time limit and was abandoned. Its outcome is unknown: it may already have taken effect, so inspect the current state before repeating it.' + +/** Whether the call's deadline has passed. A call without a deadline never passes one. */ +export function toolDeadlinePassed(deadlineAt: number | undefined, now: number): boolean { + return deadlineAt !== undefined && now >= deadlineAt +} + +/** + * The last moment anything may start for a call: its deadline less the minimum start budget, + * since work started later would likely be abandoned mid-flight, its effects unknown. + */ +export function lastToolStartAt(deadlineAt: number | undefined): number | undefined { + return deadlineAt === undefined ? undefined : deadlineAt - SIM_TOOL_MIN_START_BUDGET_MS +} + +/** + * Whether it is too late to start anything for the call: no handler, approval prompt, allowed + * decision, server fallback or client pickup. Exactly the minimum budget left still starts. + */ +export function toolStartWindowClosed(deadlineAt: number | undefined, now: number): boolean { + const lastStartAt = lastToolStartAt(deadlineAt) + return lastStartAt !== undefined && now > lastStartAt +} + +/** The column value for a call's deadline. */ +export function toDeadlineDate(deadlineAt: number | undefined): Date | undefined { + return deadlineAt === undefined ? undefined : new Date(deadlineAt) +} + +/** A wait of `budgetMs` starting at `now`, shortened so it ends by the call's deadline. */ +export function capToToolDeadline( + budgetMs: number, + deadlineAt: number | undefined, + now: number +): number { + return deadlineAt === undefined ? budgetMs : Math.max(0, Math.min(budgetMs, deadlineAt - now)) +} diff --git a/apps/sim/lib/mothership/request/tools/execution-lifetime.ts b/apps/sim/lib/mothership/request/tools/execution-lifetime.ts index f00f3eb197f..98031b32679 100644 --- a/apps/sim/lib/mothership/request/tools/execution-lifetime.ts +++ b/apps/sim/lib/mothership/request/tools/execution-lifetime.ts @@ -15,9 +15,9 @@ import { prepareWorkbenchAccess, recordSimSandboxProcess, renewSimToolExecutionLease, + type SimToolExecutionClaim, settleSimSandboxProcess, settleSimToolExecution, - type ToolExecutionClaim, } from '@/lib/mothership/async-runs/repository' import { TraceAttr } from '@/lib/mothership/generated/trace-attributes-v1' import { TraceSpan } from '@/lib/mothership/generated/trace-spans-v1' @@ -30,7 +30,7 @@ export interface ToolExecutionLifetime { readonly signal: AbortSignal readonly owner?: SimToolExecutionOwner complete(input: CompleteAsyncToolCallInput): Promise - claim(runId: string, userId: string): Promise + claim(runId: string, userId: string): Promise hold(work: Promise): Promise } diff --git a/apps/sim/lib/mothership/request/tools/executor.test.ts b/apps/sim/lib/mothership/request/tools/executor.test.ts index 38ce1589ef6..5bff6dff4d3 100644 --- a/apps/sim/lib/mothership/request/tools/executor.test.ts +++ b/apps/sim/lib/mothership/request/tools/executor.test.ts @@ -124,6 +124,7 @@ import { sealClientToolCompletion, sealClientToolContext, } from '@/lib/mothership/request/tools/client-completion-seal.server' +import { TOOL_DEADLINE_BEFORE_START_MESSAGE } from '@/lib/mothership/request/tools/deadline' import { buildToolExecutionContext, executeToolAndReport, @@ -302,6 +303,40 @@ describe('pendingToolWaitBudgetMs', () => { TOOL_WATCHDOG_DEFAULT_MS ) }) + + describe('when the worker named a deadline for the call', () => { + const now = new Date('2026-10-15T12:00:00Z').getTime() + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(now) + }) + afterEach(() => vi.useRealTimers()) + + it('ends a long tool, an approval and a desktop call at that deadline', () => { + const deadlineAt = now + 10 * 60_000 + expect( + pendingToolWaitBudgetMs({ name: 'generate_audio', status: 'executing', deadlineAt }) + ).toBe(10 * 60_000) + expect( + pendingToolWaitBudgetMs({ name: 'terminal_run', status: 'awaiting_approval', deadlineAt }) + ).toBe(10 * 60_000) + expect( + pendingToolWaitBudgetMs( + { name: 'terminal', status: 'executing', params: { operation: 'run' }, deadlineAt }, + 'device-1' + ) + ).toBe(10 * 60_000) + }) + + it('keeps a shorter budget of its own, and waits no longer once the deadline passed', () => { + expect( + pendingToolWaitBudgetMs({ name: 'read', status: 'executing', deadlineAt: now + 600_000 }) + ).toBe(TOOL_WATCHDOG_DEFAULT_MS) + expect( + pendingToolWaitBudgetMs({ name: 'run_code', status: 'executing', deadlineAt: now - 1 }) + ).toBe(0) + }) + }) }) describe('buildToolExecutionContext', () => { @@ -334,8 +369,10 @@ describe('executeToolAndReport provenance isolation', () => { beforeEach(() => { executeTool.mockReset() completeAsyncToolCall.mockResolvedValue(null) + completePendingAsyncToolCall.mockReset() markAsyncToolRunning.mockResolvedValue(null) upsertAsyncToolCall.mockResolvedValue(null) + claimToolExecution.mockReset() claimToolExecution.mockResolvedValue({ outcome: 'claimed' }) settleSimToolExecution.mockResolvedValue(undefined) waitForToolConfirmation.mockReset() @@ -590,12 +627,44 @@ describe('executeToolAndReport provenance isolation', () => { it('refuses a late tool after durable Stop closed admission', async () => { claimToolExecution.mockResolvedValueOnce({ outcome: 'closed' }) + completePendingAsyncToolCall.mockImplementationOnce(async (input) => ({ ...input })) const tool = buildPendingToolCall() const result = await executeToolAndReport(tool.id, buildStreamingContext(tool), { userId: 'user-1', workflowId: 'workflow-1', }) - expect(result.status).toBe('cancelled') + expect(result).toMatchObject({ + status: 'cancelled', + message: 'Run stopped before tool admission', + data: { cancelled: true }, + }) + expect(completePendingAsyncToolCall).toHaveBeenCalledExactlyOnceWith({ + toolCallId: tool.id, + status: 'cancelled', + result: { cancelled: true }, + error: 'Run stopped before tool admission', + }) + expect(publishToolConfirmation).toHaveBeenCalledWith( + expect.objectContaining({ toolCallId: tool.id, status: 'cancelled' }) + ) + expect(completeAsyncToolCall).not.toHaveBeenCalled() + expect(executeTool).not.toHaveBeenCalled() + expect(settleSimToolExecution).not.toHaveBeenCalled() + }) + + it('keeps the result of a call another controller started before Stop closed admission', async () => { + claimToolExecution.mockResolvedValueOnce({ outcome: 'closed' }) + completePendingAsyncToolCall.mockResolvedValueOnce(null) + waitForToolConfirmation.mockResolvedValueOnce({ status: 'success', data: { ran: true } }) + const tool = buildPendingToolCall() + const result = await executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + }) + expect(result.status).toBe('success') + expect(tool.result?.output).toEqual({ ran: true }) + expect(completeAsyncToolCall).not.toHaveBeenCalled() + expect(publishToolConfirmation).not.toHaveBeenCalled() expect(executeTool).not.toHaveBeenCalled() expect(settleSimToolExecution).not.toHaveBeenCalled() }) @@ -620,6 +689,176 @@ describe('executeToolAndReport provenance isolation', () => { expect(settleSimToolExecution).toHaveBeenCalledExactlyOnceWith(tool.id, expect.any(String)) }) + it('aborts a hung long tool at the call deadline instead of its own watchdog', async () => { + vi.useFakeTimers() + let signal: AbortSignal | undefined + executeTool.mockImplementation(async (_name, _params, context: ExecutionContext) => { + signal = context.abortSignal + return new Promise(() => {}) + }) + const tool: ToolCallState = { + ...buildPendingToolCall(), + name: 'generate_audio', + deadlineAt: Date.now() + 120_000, + } + const pending = executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + }) + + await vi.advanceTimersByTimeAsync(120_000) + + expect(signal?.aborted).toBe(true) + expect((await pending).status).toBe('error') + expect(upsertAsyncToolCall).toHaveBeenCalledWith( + expect.objectContaining({ executionDeadlineAt: new Date(tool.deadlineAt ?? 0) }) + ) + }) + + it('never starts a handler once the call deadline has passed', async () => { + const tool: ToolCallState = { + ...buildPendingToolCall(), + name: 'generate_audio', + deadlineAt: Date.now() - 1, + } + const completion = await executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + }) + + expect(executeTool).not.toHaveBeenCalled() + expect(completion.status).toBe('error') + expect(tool.error).toBe(TOOL_DEADLINE_BEFORE_START_MESSAGE) + }) + + it('settles a call its claim found too late to start as never run, without claiming it', async () => { + claimToolExecution.mockResolvedValueOnce({ outcome: 'too_late' }) + completePendingAsyncToolCall.mockImplementationOnce(async (input) => ({ ...input })) + const tool: ToolCallState = { + ...buildPendingToolCall(), + name: 'generate_audio', + deadlineAt: Date.now() + 30_000, + } + const completion = await executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + }) + + expect(executeTool).not.toHaveBeenCalled() + expect(completePendingAsyncToolCall).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + toolCallId: tool.id, + status: 'failed', + error: TOOL_DEADLINE_BEFORE_START_MESSAGE, + }) + ) + expect(completion.status).toBe('error') + expect(tool.result?.output).toMatchObject({ + error: TOOL_DEADLINE_BEFORE_START_MESSAGE, + notStarted: true, + }) + }) + + it('settles the call of a stopped run as stopped even past its start window', async () => { + claimToolExecution.mockResolvedValueOnce({ outcome: 'closed' }) + completePendingAsyncToolCall.mockImplementation(async (input) => ({ ...input })) + const tool: ToolCallState = { + ...buildPendingToolCall(), + name: 'generate_audio', + deadlineAt: Date.now() + 30_000, + } + const completion = await executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + }) + + expect(completePendingAsyncToolCall).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ status: 'cancelled', error: 'Run stopped before tool admission' }) + ) + expect(completion).toMatchObject({ + status: 'cancelled', + message: 'Run stopped before tool admission', + }) + }) + + it('claims a call without a deadline without trying to settle it first', async () => { + executeTool.mockResolvedValue({ success: true, output: {} }) + const tool: ToolCallState = { ...buildPendingToolCall(), name: 'generate_audio' } + await executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + }) + + expect(completePendingAsyncToolCall).not.toHaveBeenCalled() + expect(claimToolExecution).toHaveBeenCalledOnce() + expect(executeTool).toHaveBeenCalledOnce() + }) + + it('runs a call again only until the deadline recorded when it was first handed', async () => { + vi.useFakeTimers() + let signal: AbortSignal | undefined + executeTool.mockImplementation(async (_name, _params, context: ExecutionContext) => { + signal = context.abortSignal + return new Promise(() => {}) + }) + upsertAsyncToolCall.mockResolvedValueOnce({ + executionDeadlineAt: new Date(Date.now() + 90_000), + }) + const tool: ToolCallState = { + ...buildPendingToolCall(), + name: 'generate_audio', + deadlineAt: Date.now() + 15 * 60_000, + } + const pending = executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + }) + + await vi.advanceTimersByTimeAsync(90_000) + + expect(signal?.aborted).toBe(true) + expect((await pending).status).toBe('error') + }) + + it('starts a handler with exactly the minimum budget of its deadline left', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2026-10-15T12:00:00Z')) + executeTool.mockResolvedValue({ success: true, output: {} }) + const tool: ToolCallState = { + ...buildPendingToolCall(), + name: 'generate_audio', + deadlineAt: Date.now() + 60_000, + } + await executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + }) + + expect(executeTool).toHaveBeenCalledOnce() + }) + + it('does not start a handler with less than a minute of its deadline left', async () => { + const tool: ToolCallState = { + ...buildPendingToolCall(), + name: 'generate_audio', + deadlineAt: Date.now() + 30_000, + } + const completion = await executeToolAndReport(tool.id, buildStreamingContext(tool), { + userId: 'user-1', + workflowId: 'workflow-1', + resolvedSecretTraceRegistry: new ResolvedSecretTraceRegistry(), + }) + + expect(executeTool).not.toHaveBeenCalled() + expect(completion.status).toBe('error') + expect(tool.error).toBe(TOOL_DEADLINE_BEFORE_START_MESSAGE) + }) + it('aborts a timed-out handler without cancelling a parallel tool', async () => { vi.useFakeTimers() const parent = new AbortController() diff --git a/apps/sim/lib/mothership/request/tools/executor.ts b/apps/sim/lib/mothership/request/tools/executor.ts index 0a4de07afbd..361d143963e 100644 --- a/apps/sim/lib/mothership/request/tools/executor.ts +++ b/apps/sim/lib/mothership/request/tools/executor.ts @@ -11,6 +11,7 @@ import type { } from '@/lib/mothership/async-runs/lifecycle' import { type CompleteAsyncToolCallInput, + completePendingAsyncToolCall, getAsyncToolCall, markAsyncToolRunning, upsertAsyncToolCall, @@ -62,6 +63,12 @@ import { setTerminalToolCallState, } from '@/lib/mothership/request/tool-call-state' import { settleToolCallFailure } from '@/lib/mothership/request/tools/call-failure' +import { + capToToolDeadline, + TOOL_DEADLINE_BEFORE_START_MESSAGE, + toDeadlineDate, + toolStartWindowClosed, +} from '@/lib/mothership/request/tools/deadline' import { desktopToolNotStarted } from '@/lib/mothership/request/tools/desktop-wait' import { type ToolExecutionLifetime, @@ -248,17 +255,33 @@ export function toolWatchdogTimeoutMs(toolName: string | undefined): number { : TOOL_WATCHDOG_DEFAULT_MS } +type PendingToolCall = Pick & + Partial> + /** - * How long the resume gate may wait on one pending tool call. Permission - * prompts wait as long as the permission wait itself. Browser calls share the renderer's - * budget so authorization and native queueing cannot outlive the resume gate. A desktop call on a - * device-bound run gets the full client budget: its wait fails it once the device is offline, its - * pickup window closes or its lease lapses, and otherwise it runs as long as the device renews it. + * How long the resume gate may wait on one pending tool call, from now: its own budget below, + * ended by the call's deadline when the worker named one. */ export function pendingToolWaitBudgetMs( - toolCall: - | (Pick & Partial>) - | undefined, + toolCall: PendingToolCall | undefined, + desktopDeviceId?: string | null +): number { + return capToToolDeadline( + ownPendingToolWaitBudgetMs(toolCall, desktopDeviceId), + toolCall?.deadlineAt, + Date.now() + ) +} + +/** + * Permission prompts wait as long as the permission wait itself. Browser calls share the + * renderer's budget so authorization and native queueing cannot outlive the resume gate. A + * desktop call on a device-bound run gets the full client budget: its wait fails it once the + * device is offline, its pickup window closes or its lease lapses, and otherwise it runs as long + * as the device renews it. + */ +function ownPendingToolWaitBudgetMs( + toolCall: PendingToolCall | undefined, desktopDeviceId?: string | null ): number { if (toolCall?.status === 'awaiting_approval') return PERMISSION_WAIT_TIMEOUT_MS @@ -353,7 +376,15 @@ async function executeToolWithWatchdog( // The frame's wire name can be a display identity (the worker's cli_* names); // execution always dispatches on the model's real tool name. const executableName = toolCall.execName ?? toolCall.name - const timeoutMs = toolWatchdogTimeoutMs(executableName) + const now = Date.now() + // Admission or approval can leave too little time to finish; the handler then never starts. + if (toolStartWindowClosed(toolCall.deadlineAt, now)) + throw new Error(TOOL_DEADLINE_BEFORE_START_MESSAGE) + const timeoutMs = capToToolDeadline( + toolWatchdogTimeoutMs(executableName), + toolCall.deadlineAt, + now + ) const controller = new AbortController() lifetime.signal.throwIfAborted() const signal = AbortSignal.any([ @@ -466,6 +497,25 @@ export async function failPendingToolCall( }) } +/** + * Fails a call with `message` only while nothing has claimed it, so it provably never started. + * Reports whether that applied. + */ +export async function failUnstartedToolCall( + toolCallId: string, + context: StreamingContext, + execContext: ExecutionContext, + message: string +): Promise { + const toolCall = context.toolCalls.get(toolCallId) + if (!toolCall || toolCall.endTime || isTerminalToolCallStatus(toolCall.status)) return false + return settleAbandonedToolCall(toolCall, context, execContext, { + message, + data: { error: message, notStarted: true }, + unclaimedOnly: true, + }) +} + /** * What a desktop call that lost its result tells the model. Only a local read can be in flight * without a claim (an older desktop reads without claiming), and such a read may never have @@ -660,6 +710,8 @@ export async function executeToolAndReport( ) } +type CancelledSpanDetail = { cancelReason: string; error?: string | undefined } + async function executeToolAndReportInner( toolCall: ToolCallState, context: StreamingContext, @@ -692,20 +744,39 @@ async function executeToolAndReportInner( } toolCall.status = 'executing' - await upsertAsyncToolCall({ + const record = await upsertAsyncToolCall({ runId: context.runId, toolCallId: toolCall.id, ...(toolCall.targetWorkspaceId ? { targetWorkspaceId: toolCall.targetWorkspaceId } : {}), toolName: toolCall.name, args: toolCall.params, + executionDeadlineAt: toDeadlineDate(toolCall.deadlineAt), }).catch((err) => { if (err instanceof AsyncToolCallOwnershipError) throw err throw new Error('Tool could not start because its execution record is unavailable') }) + /** A re-run after a takeover gets what remains of the call's first deadline, never a fresh one. */ + if (record?.executionDeadlineAt) toolCall.deadlineAt = record.executionDeadlineAt.getTime() if (context.runId) { const claim = await lifetime.claim(context.runId, execContext.userId) - if (claim.outcome === 'closed') return settleCancelled('Run stopped before tool admission') - if (claim.outcome === 'existing') { + if (claim.outcome === 'closed') { + /** A call another controller already started keeps its own result, observed below. */ + const stopped = await settleCancelledIfUnstarted('Run stopped before tool admission') + if (stopped) return stopped + } + // Settled as never run instead of claimed, so a crash before the handler refuses to start + // cannot leave it recovered as an unknown outcome. Losing that settlement leaves another writer's. + if ( + claim.outcome === 'too_late' && + (await failUnstartedToolCall( + toolCall.id, + context, + execContext, + TOOL_DEADLINE_BEFORE_START_MESSAGE + )) + ) + return terminalCompletionFromToolCall(toolCall) + if (claim.outcome !== 'claimed') { /** The winning controller owns execution; this promise observes its durable result. */ const completion = await waitForToolConfirmation( toolCall.id, @@ -771,21 +842,39 @@ async function executeToolAndReportInner( // never evaluated (and the span does not exist yet there anyway). async function settleCancelled( message: string, - span?: { cancelReason: string; error?: string | undefined } + span?: CancelledSpanDetail ): Promise { markToolCallCancelled(message) const cancellationResult = toolCall.result markToolResultSeen(context, toolCall.id) - await lifetime.complete({ + await lifetime.complete(cancellationRecord(message)) + if (toolCall.result !== cancellationResult) { + return terminalCompletionFromToolCall(toolCall) + } + return publishCancellation(message, span) + } + + /** Cancels the call only while nothing has started it; otherwise reports nothing settled. */ + async function settleCancelledIfUnstarted( + message: string + ): Promise { + if (!(await completePendingAsyncToolCall(cancellationRecord(message)))) return undefined + markToolCallCancelled(message) + markToolResultSeen(context, toolCall.id) + return publishCancellation(message) + } + + function cancellationRecord(message: string): CompleteAsyncToolCallInput { + return { toolCallId: toolCall.id, ...(toolCall.targetWorkspaceId ? { targetWorkspaceId: toolCall.targetWorkspaceId } : {}), status: MothershipStreamV1AsyncToolRecordStatus.cancelled, result: { cancelled: true }, error: message, - }) - if (toolCall.result !== cancellationResult) { - return terminalCompletionFromToolCall(toolCall) } + } + + function publishCancellation(message: string, span?: CancelledSpanDetail): AsyncCompletionSignal { publishTerminalToolConfirmation({ toolCallId: toolCall.id, ...(toolCall.targetWorkspaceId ? { targetWorkspaceId: toolCall.targetWorkspaceId } : {}), diff --git a/apps/sim/lib/mothership/request/tools/permission.test.ts b/apps/sim/lib/mothership/request/tools/permission.test.ts index a8d472ed21a..e5d74997645 100644 --- a/apps/sim/lib/mothership/request/tools/permission.test.ts +++ b/apps/sim/lib/mothership/request/tools/permission.test.ts @@ -22,6 +22,7 @@ vi.mock('@/lib/mothership/persistence/tool-permission', async (importOriginal) = import { MothershipStreamV1ToolExecutor } from '@/lib/mothership/generated/mothership-stream-v1' import { createStreamingContext } from '@/lib/mothership/request/context/request-context' +import { TOOL_DEADLINE_BEFORE_START_MESSAGE } from '@/lib/mothership/request/tools/deadline' import { runGatedToolExecution, toolCallNeedsApproval, @@ -205,6 +206,84 @@ describe('runGatedToolExecution', () => { toolRequiresApproval.mockReturnValue(true) }) + it('opens no prompt once the call deadline has passed', async () => { + const toolCall = { ...makeToolCall(), deadlineAt: Date.now() - 1 } + const execute = vi.fn() + + expect(await gate(makeContext(), toolCall, execute, [])).toMatchObject({ status: 'error' }) + expect(waitForToolPermissionDecision).not.toHaveBeenCalled() + expect(execute).not.toHaveBeenCalled() + }) + + it('does not honor an approval that lands after the call deadline', async () => { + vi.useFakeTimers() + try { + waitForToolPermissionDecision.mockImplementation( + () => + new Promise((resolve) => + setTimeout(() => resolve({ toolCallId: 'call-1', decision: 'allow' }), 2_000) + ) + ) + const toolCall = { ...makeToolCall(), deadlineAt: Date.now() + 1_000 } + const execute = vi.fn() + const pending = gate(makeContext(), toolCall, execute, []) + + await vi.advanceTimersByTimeAsync(2_000) + + expect(await pending).toMatchObject({ status: 'error' }) + expect(execute).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) + + it('does not honor an approval that leaves less than a minute to run the call', async () => { + vi.useFakeTimers() + try { + waitForToolPermissionDecision.mockImplementation( + () => + new Promise((resolve) => + setTimeout(() => resolve({ toolCallId: 'call-1', decision: 'allow' }), 40_000) + ) + ) + const toolCall = { ...makeToolCall(), deadlineAt: Date.now() + 90_000 } + const execute = vi.fn() + const pending = gate(makeContext(), toolCall, execute, []) + + await vi.advanceTimersByTimeAsync(40_000) + + expect(await pending).toMatchObject({ status: 'error' }) + expect(execute).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) + + it('stops waiting for an answer at the deadline the worker named for the call', async () => { + vi.useFakeTimers() + try { + waitForToolPermissionDecision.mockImplementation( + (_toolCallId: string, timeoutMs: number) => + new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)) + ) + const toolCall = { ...makeToolCall(), deadlineAt: Date.now() + 5 * 60_000 } + const execute = vi.fn() + let settled = false + const pending = gate(makeContext(), toolCall, execute, []).finally(() => { + settled = true + }) + + await vi.advanceTimersByTimeAsync(5 * 60_000) + + expect(settled).toBe(true) + expect(await pending).toMatchObject({ status: 'error' }) + expect(toolCall.error).toBe(TOOL_DEADLINE_BEFORE_START_MESSAGE) + expect(execute).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) + it('does not execute the tool until the user has answered', async () => { const context = makeContext() const toolCall = makeToolCall() diff --git a/apps/sim/lib/mothership/request/tools/permission.ts b/apps/sim/lib/mothership/request/tools/permission.ts index 850a1f5da87..a62c965ff86 100644 --- a/apps/sim/lib/mothership/request/tools/permission.ts +++ b/apps/sim/lib/mothership/request/tools/permission.ts @@ -21,6 +21,13 @@ import { import { withCopilotSpan } from '@/lib/mothership/request/otel' import { markToolResultSeen } from '@/lib/mothership/request/sse-utils' import { setTerminalToolCallState } from '@/lib/mothership/request/tool-call-state' +import { + capToToolDeadline, + lastToolStartAt, + TOOL_DEADLINE_BEFORE_START_MESSAGE, + toolDeadlinePassed, + toolStartWindowClosed, +} from '@/lib/mothership/request/tools/deadline' import type { OrchestratorOptions, StreamingContext, @@ -246,11 +253,19 @@ export function runGatedToolExecution( return { status: MothershipStreamV1ToolOutcome.success, message: output.message } } - const decision = await waitForToolPermissionDecision( - toolCallId, - PERMISSION_WAIT_TIMEOUT_MS, - options.abortSignal - ) + // No prompt opens once it is too late to start the call, and an answer after that is not honored. + const answer = toolStartWindowClosed(toolCall.deadlineAt, Date.now()) + ? null + : await waitForToolPermissionDecision( + toolCallId, + capToToolDeadline( + PERMISSION_WAIT_TIMEOUT_MS, + lastToolStartAt(toolCall.deadlineAt), + Date.now() + ), + options.abortSignal + ) + const decision = toolStartWindowClosed(toolCall.deadlineAt, Date.now()) ? null : answer if (!decision) { // Timed out or the turn was stopped. Fail the call rather than running @@ -259,7 +274,9 @@ export function runGatedToolExecution( span.setAttribute(TraceAttr.ToolOutcome, MothershipStreamV1ToolOutcome.cancelled) const error = options.abortSignal?.aborted ? 'Stopped before the user approved this tool.' - : 'Timed out waiting for the user to approve this tool.' + : toolDeadlinePassed(lastToolStartAt(toolCall.deadlineAt), Date.now()) + ? TOOL_DEADLINE_BEFORE_START_MESSAGE + : 'Timed out waiting for the user to approve this tool.' setTerminalToolCallState(toolCall, { status: MothershipStreamV1ToolOutcome.cancelled, output: { error }, diff --git a/apps/sim/lib/mothership/request/tools/workflow-client-fallback.test.ts b/apps/sim/lib/mothership/request/tools/workflow-client-fallback.test.ts index cea0caa9dee..ec29de68d56 100644 --- a/apps/sim/lib/mothership/request/tools/workflow-client-fallback.test.ts +++ b/apps/sim/lib/mothership/request/tools/workflow-client-fallback.test.ts @@ -102,6 +102,29 @@ describe('raceWorkflowToolClientPickup', () => { expect(waiterSignals.at(0)?.aborted).toBe(true) }) + it('does not take a call over on the server once its deadline has passed', async () => { + waitForWorkflowToolCompletion.mockResolvedValue(null) + claimWorkflowToolExecution.mockResolvedValue({ toolCallId: 'tool-1' }) + const params = baseParams({ timeoutMs: 0, deadlineAt: Date.now() }) + + const outcome = await raceWorkflowToolClientPickup(params as never) + + expect(outcome).toEqual({ winner: 'client', completion: null }) + expect(claimWorkflowToolExecution).not.toHaveBeenCalled() + expect(params.runOnServer).not.toHaveBeenCalled() + }) + + it('does not take a call over with less than a minute of its deadline left', async () => { + waitForWorkflowToolCompletion.mockResolvedValue(null) + claimWorkflowToolExecution.mockResolvedValue({ toolCallId: 'tool-1' }) + const params = baseParams({ timeoutMs: 0, deadlineAt: Date.now() + 30_000 }) + + await raceWorkflowToolClientPickup(params as never) + + expect(claimWorkflowToolExecution).not.toHaveBeenCalled() + expect(params.runOnServer).not.toHaveBeenCalled() + }) + it('claims immediately when the caller declared no client pickup (graceMs 0)', async () => { // ChatRequest.clientCapabilities without 'workflow-tool-pickup' resolves to graceMs 0 // at the dispatch site: same claim arbitration, no dead-air grace for a client that diff --git a/apps/sim/lib/mothership/request/tools/workflow-client-fallback.ts b/apps/sim/lib/mothership/request/tools/workflow-client-fallback.ts index ef7ca908a6a..0c346b84418 100644 --- a/apps/sim/lib/mothership/request/tools/workflow-client-fallback.ts +++ b/apps/sim/lib/mothership/request/tools/workflow-client-fallback.ts @@ -10,6 +10,7 @@ import { claimWorkflowToolExecution } from '@/lib/mothership/async-runs/reposito import { CopilotDegradedReason } from '@/lib/mothership/generated/trace-attribute-values-v1' import { recordDegraded } from '@/lib/mothership/request/metrics' import { waitForWorkflowToolCompletion } from '@/lib/mothership/request/tools/client' +import { toolStartWindowClosed } from '@/lib/mothership/request/tools/deadline' import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' const logger = createLogger('CopilotWorkflowClientFallback') @@ -32,6 +33,8 @@ interface RaceWorkflowToolClientPickupParams { toolCallId: string workflowId?: string timeoutMs: number + /** When the call's deadline passes: past it the server does not take the call over. */ + deadlineAt?: number graceMs: number abortSignal?: AbortSignal registry?: ResolvedSecretTraceRegistry @@ -95,8 +98,8 @@ export async function raceWorkflowToolClientPickup( return { winner: 'client', completion: first.completion } } - // Never claim work on a turn the user already stopped. - if (abortSignal?.aborted) { + // Never claim work on a turn the user already stopped, or too late to start the call. + if (abortSignal?.aborted || toolStartWindowClosed(params.deadlineAt, Date.now())) { return { winner: 'client', completion: await clientWait } } diff --git a/apps/sim/lib/mothership/request/types.ts b/apps/sim/lib/mothership/request/types.ts index f4ee011294a..7ee453ecb4a 100644 --- a/apps/sim/lib/mothership/request/types.ts +++ b/apps/sim/lib/mothership/request/types.ts @@ -49,6 +49,12 @@ export interface ToolCallState { error?: string startTime?: number endTime?: number + /** + * Epoch ms by which every wait and execution for this call must end: from the frame's + * `deadlineMs`, or the persisted deadline of a call handed before a takeover. Undefined when + * the worker named none. + */ + deadlineAt?: number /** * For a subagent-scoped tool call, the invoking subagent's channel id (its * outer tool_use id, = event.scope.parentToolCallId). Captured at dispatch so diff --git a/apps/sim/lib/mothership/tools/client/computer-tool-result.test.ts b/apps/sim/lib/mothership/tools/client/computer-tool-result.test.ts index 4ccd34fdccb..4eb5a6c0d70 100644 --- a/apps/sim/lib/mothership/tools/client/computer-tool-result.test.ts +++ b/apps/sim/lib/mothership/tools/client/computer-tool-result.test.ts @@ -117,7 +117,7 @@ describe('native computer model projection', () => { const result = computerToolResultForModel(nested ? sequence(state) : state) const textResult = { ...result, observations: undefined } const serialized = JSON.stringify(textResult) - expect(new TextEncoder().encode(serialized).length).toBeLessThan(44 * 1024) + expect(new TextEncoder().encode(serialized).length).toBeLessThan(22 * 1024) expect(serialized).toContain('editor-late AXTextArea parent=e782') expect(serialized).toContain('e782 AXStaticText parent=e0') expect(serialized).toContain('send-late AXButton parent=e782') @@ -193,7 +193,7 @@ describe('native computer model projection', () => { expect(ids.size).toBeLessThan(state.nodes.length) for (const match of tree.matchAll(/\bparent=(\S+)/g)) expect(ids.has(match[1])).toBe(true) expect(Buffer.byteLength(JSON.stringify({ ...result, observations: undefined }))).toBeLessThan( - 44 * 1024 + 22 * 1024 ) expect(result).toMatchObject({ dispatched: true, sequence: { completedSteps: 1 } }) }) diff --git a/apps/sim/lib/mothership/tools/client/computer-tool-result.ts b/apps/sim/lib/mothership/tools/client/computer-tool-result.ts index 3809a431490..8fb6de95690 100644 --- a/apps/sim/lib/mothership/tools/client/computer-tool-result.ts +++ b/apps/sim/lib/mothership/tools/client/computer-tool-result.ts @@ -3,7 +3,9 @@ import { truncateAtCodePoint } from '@sim/utils/string' // Keep encoded pixels below the 10 MiB completion body cap, with room for state and its envelope. const MAX_SCREENSHOT_BASE64_BYTES = 9 * 1024 * 1024 -const MAX_STATE_BYTES = 44 * 1024 +// Mothership stores a computer result over 24,000 characters and shows the model a preview cut +// through the middle of the tree; staying under it keeps the prioritized rows the model needs. +const MAX_STATE_BYTES = 22 * 1024 const encoder = new TextEncoder() /** Screenshots are image observations; a bounded readable tree keeps actionable references inline. */ @@ -105,8 +107,10 @@ function snapshotForModel(result: ComputerUseSnapshot, envelopeBytes = 0) { const primaryAction = node.actions.some( (action) => action === 'AXPress' || action === 'AXConfirm' || action === 'AXPick' ) + /** Items of an open menu have an on-screen frame; closed menus report none. */ + const openMenuItem = node.role === 'AXMenuItem' && (node.height ?? 0) > 0 return ( - (node.focused + (node.focused || openMenuItem ? 100 : node.editable ? 80 diff --git a/apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts b/apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts index 3e1afd3576f..5f9acf767cb 100644 --- a/apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts +++ b/apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts @@ -332,6 +332,20 @@ describe.runIf(Boolean(redisUrl))('desktop tool calls the server no longer admit expect(await storedCall(toolCallId)).toMatchObject({ status: 'pending', claimedBy: null }) }) + it('refuses the claim with less than a minute of the call deadline left', async () => { + const { runId } = await startRun() + const toolCallId = await agentCalls(runId, 'terminal', { operation: 'read' }) + await db + .update(copilotAsyncToolCalls) + .set({ executionDeadlineAt: new Date(Date.now() + 30_000) }) + .where(eq(copilotAsyncToolCalls.toolCallId, toolCallId)) + + expect( + await claimDesktopToolCall({ toolCallId, runId, userId, claimedBy: 'desktop-terminal' }) + ).toEqual({ outcome: 'existing' }) + expect(await storedCall(toolCallId)).toMatchObject({ status: 'pending', claimedBy: null }) + }) + it('wakes the waiting turn with what Stop did to a call the desktop already claimed', async () => { const { runId, streamId } = await startRun() const registry = new ResolvedSecretTraceRegistry([]) diff --git a/apps/sim/lib/mothership/tools/server/media/generate-audio.ts b/apps/sim/lib/mothership/tools/server/media/generate-audio.ts index ffab97f3143..2a2d03dd8cc 100644 --- a/apps/sim/lib/mothership/tools/server/media/generate-audio.ts +++ b/apps/sim/lib/mothership/tools/server/media/generate-audio.ts @@ -130,10 +130,10 @@ export const generateAudioServerTool: BaseServerTool recordServiceCost('falai_audio', cost.costDollars), }) - await recordServiceCost('falai_audio', result.cost.costDollars) - const outputFile = params.outputs?.files?.[0] const ext = audioExtFromContentType(result.contentType) const outputPath = outputFile?.path || `files/generated-audio.${ext}` diff --git a/apps/sim/lib/mothership/tools/server/media/generate-video.ts b/apps/sim/lib/mothership/tools/server/media/generate-video.ts index 1ad4bef38b5..e563fa702b6 100644 --- a/apps/sim/lib/mothership/tools/server/media/generate-video.ts +++ b/apps/sim/lib/mothership/tools/server/media/generate-video.ts @@ -107,10 +107,10 @@ export const generateVideoServerTool: BaseServerTool recordServiceCost('falai_video', cost.costDollars), }) - await recordServiceCost('falai_video', result.cost.costDollars) - const outputFile = params.outputs?.files?.[0] const outputPath = outputFile?.path || 'files/generated-video.mp4' const mode = outputFile?.mode ?? 'create' diff --git a/apps/sim/lib/mothership/tools/server/media/model-boundaries.test.ts b/apps/sim/lib/mothership/tools/server/media/model-boundaries.test.ts index 2e2d379bfb7..96f62494678 100644 --- a/apps/sim/lib/mothership/tools/server/media/model-boundaries.test.ts +++ b/apps/sim/lib/mothership/tools/server/media/model-boundaries.test.ts @@ -36,6 +36,20 @@ import { generateAudioServerTool } from '@/lib/mothership/tools/server/media/gen import { generateVideoServerTool } from '@/lib/mothership/tools/server/media/generate-video' import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' +interface FalGenerationParams { + signal?: AbortSignal + onCost: (cost: { costDollars: number }) => Promise +} + +/** A fal generation that bills 0.1 through `onCost`, then returns `media` with that cost. */ +function billsThen(media: Record) { + return async ({ onCost }: FalGenerationParams) => { + const cost = { costDollars: 0.1 } + await onCost(cost) + return { ...media, cost } + } +} + function contextWithSecrets( entries: Array<{ name: string; plaintext: string }> ): ServerToolContext { @@ -67,19 +81,17 @@ describe('Mothership media model boundaries', () => { { content: { parts: [{ inlineData: { data: 'aW1hZ2U=', mimeType: 'image/png' } }] } }, ], }) - mockGenerateFalVideo.mockResolvedValue({ - buffer: Buffer.from('video'), - contentType: 'video/mp4', - model: 'veo-3.1-fast', - cost: { costDollars: 0.1 }, - }) - mockGenerateFalAudio.mockResolvedValue({ - buffer: Buffer.from('audio'), - contentType: 'audio/mpeg', - type: 'music', - model: 'music-model', - cost: { costDollars: 0.1 }, - }) + mockGenerateFalVideo.mockImplementation( + billsThen({ buffer: Buffer.from('video'), contentType: 'video/mp4', model: 'veo-3.1-fast' }) + ) + mockGenerateFalAudio.mockImplementation( + billsThen({ + buffer: Buffer.from('audio'), + contentType: 'audio/mpeg', + type: 'music', + model: 'music-model', + }) + ) }) it('meters image, video and audio at their provider boundary before file persistence', async () => { @@ -97,6 +109,51 @@ describe('Mothership media model boundaries', () => { ]) }) + it.each([ + ['audio', () => mockGenerateFalAudio, generateAudioServerTool], + ['video', () => mockGenerateFalVideo, generateVideoServerTool], + ] as const)( + 'ends the %s generation when the tool call is aborted', + async (_kind, generator, tool) => { + generator().mockImplementationOnce( + ({ signal }: { signal?: AbortSignal }) => + new Promise((_resolve, reject) => { + signal?.addEventListener('abort', () => reject(signal.reason), { once: true }) + }) + ) + const controller = new AbortController() + const result = tool.execute( + { prompt: 'p' }, + { ...contextWithSecrets([]), abortSignal: controller.signal } + ) + + controller.abort(new Error('Tool timed out')) + + await expect(result).resolves.toMatchObject({ success: false }) + } + ) + + it.each([ + ['audio', 'falai_audio', mockGenerateFalAudio, generateAudioServerTool], + ['video', 'falai_video', mockGenerateFalVideo, generateVideoServerTool], + ] as const)( + 'bills a completed %s generation once when the tool call is aborted after it', + async (_kind, service, generator, tool) => { + generator.mockImplementationOnce(async ({ onCost }: FalGenerationParams) => { + await onCost({ costDollars: 0.2 }) + throw new Error('Tool timed out') + }) + const charges = vi.fn().mockResolvedValue(undefined) + + const result = await observeServiceCosts(charges, () => + tool.execute({ prompt: 'p' }, contextWithSecrets([])) + ) + + expect(result).toMatchObject({ success: false }) + expect(charges.mock.calls).toEqual([[service, 0.2]]) + } + ) + it('preserves image prompts that merely collide with ambient secret plaintext', async () => { const context = contextWithSecrets([{ name: 'PROMPT', plaintext: 'private prompt' }]) diff --git a/apps/sim/lib/mothership/transport/control.ts b/apps/sim/lib/mothership/transport/control.ts index e52a7e67d3c..31e4338195a 100644 --- a/apps/sim/lib/mothership/transport/control.ts +++ b/apps/sim/lib/mothership/transport/control.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { asOrchestrationError, statusForOrchestrationError } from '@/lib/core/orchestration/types' +import { HttpError } from '@/lib/core/utils/http-error' import { createTrustedCopilotPrincipal, createTrustedOrganizationCopilotPrincipal, @@ -12,6 +13,10 @@ import { INTEGRATION_CATALOG_AUDIENCE, readIntegrationCatalog, } from '@/lib/mothership/integrations/application/catalog' +import { + MEMORY_NOTES_ACCESS_AUDIENCE, + resolveMemoryNotesAccess, +} from '@/lib/mothership/memories/application/resolve-access' import { MEMORY_SCOPE_AUDIENCE, readMemoryScope, @@ -45,15 +50,17 @@ export async function executeSimControl(request: SimControlRequest): Promise ({ executeSimControl: execut vi.mock('@/lib/mothership/server/agent-url', () => mothershipAgentUrlMock) vi.mock('@sim/utils/helpers', () => utilsHelpersMock) +import { SIM_CONTROL_KINDS } from '@/lib/mothership/generated/sim-transport' import { receiveSimControls, startSimReceivers } from '@/lib/mothership/transport/receiver' const mocks = { @@ -87,18 +89,92 @@ describe('outbound receiver lifecycle', () => { expect(mocks.sleep).toHaveBeenCalledOnce() }) - it('does not execute malformed or redirected responses as Sim operations', async () => { + it('does not execute redirected responses or requests it cannot read as Sim operations', async () => { const controller = new AbortController() + const replies: string[] = [] mocks.fetch .mockResolvedValueOnce(new Response(null, { status: 302 })) .mockResolvedValueOnce(Response.json({ requests: [{ operation: { kind: 'unknown' } }] })) - .mockImplementationOnce(async () => { + .mockImplementation(async (url: string, init: RequestInit) => { + if (url.endsWith('/reply')) { + replies.push(String(init.body)) + return Response.json({ accepted: true }) + } controller.abort() throw new Error('shutdown') }) await receiveSimControls('https://worker.test', 'a'.repeat(64), controller.signal) expect(mocks.execute).not.toHaveBeenCalled() - expect(mocks.sleep).toHaveBeenCalledTimes(2) + expect(replies).toEqual([]) + expect(mocks.sleep).toHaveBeenCalledOnce() + expect(getMockLogger('MothershipTransportReceiver').warn).toHaveBeenCalledWith( + 'Unreadable Sim control request dropped' + ) + }) + + it('answers an unknown kind 501 and a malformed input 400 while their batch-mates still run', async () => { + const controller = new AbortController() + const valid = request() + const unknown = { ...request(), operation: { kind: 'a_future_kind', input: {} } } + const malformed = { ...request(), operation: { kind: 'run_control', input: { chatId: 7 } } } + const kindless = { ...request(), operation: { input: {} } } + const replies: { id: string; result: { status: number; body: string } }[] = [] + let polls = 0 + mocks.fetch.mockImplementation(async (url: string, init: RequestInit) => { + if (url.endsWith('/reply')) { + replies.push(JSON.parse(String(init.body))) + return Response.json({ accepted: true }) + } + polls++ + if (polls === 1) return Response.json({ requests: [unknown, valid, malformed, kindless] }) + controller.abort() + throw new Error('shutdown') + }) + await receiveSimControls('https://worker.test', 'a'.repeat(64), controller.signal) + expect(mocks.execute).toHaveBeenCalledOnce() + expect(mocks.execute).toHaveBeenCalledWith(valid) + expect(replies).toHaveLength(4) + expect(replies).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: valid.id, + result: { status: 200, body: '{"stopped":false}' }, + }), + expect.objectContaining({ + id: unknown.id, + result: { + status: 501, + body: JSON.stringify({ error: 'sim_control_unsupported', kind: 'a_future_kind' }), + }, + }), + expect.objectContaining({ + id: malformed.id, + result: { status: 400, body: '{"error":"Invalid Sim control request"}' }, + }), + expect.objectContaining({ + id: kindless.id, + result: { status: 400, body: '{"error":"Invalid Sim control request"}' }, + }), + ]) + ) + expect(mocks.sleep).not.toHaveBeenCalled() + }) + + it('declares on every poll exactly the kinds its generated contract parses', async () => { + const controller = new AbortController() + const polls: unknown[] = [] + mocks.fetch.mockImplementation(async (_url: string, init: RequestInit) => { + polls.push(JSON.parse(String(init.body))) + if (polls.length === 1) throw new Error('connection lost') + controller.abort() + throw new Error('shutdown') + }) + await receiveSimControls('https://worker.test', 'a'.repeat(64), controller.signal) + expect(SIM_CONTROL_KINDS).toContain('memory_scope') + expect(polls).toEqual([ + { channelId: 'a'.repeat(64), accepts: SIM_CONTROL_KINDS }, + { channelId: 'a'.repeat(64), accepts: SIM_CONTROL_KINDS }, + ]) }) it('does not repeat an operation when its reply acknowledgement is lost', async () => { diff --git a/apps/sim/lib/mothership/transport/receiver.ts b/apps/sim/lib/mothership/transport/receiver.ts index 8eeeb1ac947..ac1892366b8 100644 --- a/apps/sim/lib/mothership/transport/receiver.ts +++ b/apps/sim/lib/mothership/transport/receiver.ts @@ -1,9 +1,18 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' +import { truncate } from '@sim/utils/string' import { env } from '@/lib/core/config/env' import { isMothershipBenchmarkEnabled } from '@/lib/core/config/env-flags' -import { SimChannelBatch, type SimConnection } from '@/lib/mothership/generated/sim-transport' +import { + SIM_CONTROL_KINDS, + SIM_CONTROL_UNSUPPORTED, + SimChannelBatchEnvelope, + type SimConnection, + SimControlRequest, + SimControlRequestHeader, + type SimControlResult, +} from '@/lib/mothership/generated/sim-transport' import { fetchGo } from '@/lib/mothership/request/go/fetch' import { mothershipRequestHeaders } from '@/lib/mothership/request/headers' import { getMothershipBaseURL } from '@/lib/mothership/server/agent-url' @@ -13,17 +22,52 @@ import { executeSimControl } from '@/lib/mothership/transport/control' const logger = createLogger('MothershipTransportReceiver') const receivers = new Map() +interface ReceivedControl { + id: string + answer: () => Promise +} + +/** Each request parses alone, so one unknown or malformed request never costs its batch-mates. */ +function receiveControl(raw: unknown): ReceivedControl | null { + const request = SimControlRequest.safeParse(raw) + if (request.success) return { id: request.data.id, answer: () => executeSimControl(request.data) } + const header = SimControlRequestHeader.safeParse(raw) + if (!header.success) { + logger.warn('Unreadable Sim control request dropped') + return null + } + const { id, operation } = header.data + const kind = operation && truncate(operation.kind, 64) + if (operation && !SIM_CONTROL_KINDS.some((known) => known === operation.kind)) { + logger.warn('Unsupported Sim control kind', { kind }) + const body = JSON.stringify({ error: SIM_CONTROL_UNSUPPORTED, kind }) + return { id, answer: async () => ({ status: 501, body }) } + } + logger.warn('Invalid Sim control request', { + kind, + issues: request.error.issues.map((issue) => issue.path.join('.')), + }) + return { + id, + answer: async () => ({ + status: 400, + body: JSON.stringify({ error: 'Invalid Sim control request' }), + }), + } +} + export async function receiveSimControls( baseURL: string, channelId: string, signal: AbortSignal ): Promise { while (!signal.aborted) { + let phase: 'poll' | 'parse' | 'reply' = 'poll' try { const response = await fetchGo(`${baseURL}/api/sim-transport/poll`, { method: 'POST', headers: mothershipRequestHeaders(), - body: JSON.stringify({ channelId }), + body: JSON.stringify({ channelId, accepts: SIM_CONTROL_KINDS }), signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]), redirect: 'error', spanName: 'sim → worker transport poll', @@ -33,15 +77,18 @@ export async function receiveSimControls( await response.body?.cancel() throw new Error(`Transport poll refused (HTTP ${response.status})`) } - const batch = SimChannelBatch.parse(await response.json()) + phase = 'parse' + const batch = SimChannelBatchEnvelope.parse(await response.json()) + const controls = batch.requests.flatMap((raw) => receiveControl(raw) ?? []) + phase = 'reply' const replies = await Promise.allSettled( - batch.requests.map(async (request) => { + controls.map(async (control) => { signal.throwIfAborted() - const result = await executeSimControl(request) + const result = await control.answer() const reply = await fetchGo(`${baseURL}/api/sim-transport/reply`, { method: 'POST', headers: mothershipRequestHeaders(), - body: JSON.stringify({ channelId, id: request.id, result }), + body: JSON.stringify({ channelId, id: control.id, result }), redirect: 'error', signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]), spanName: 'sim → worker transport reply', @@ -56,7 +103,10 @@ export async function receiveSimControls( if (failed?.status === 'rejected') throw failed.reason } catch (error) { if (signal.aborted) return - logger.warn('Sim transport disconnected; reconnecting', { error: getErrorMessage(error) }) + logger.warn('Sim transport disconnected; reconnecting', { + phase, + error: getErrorMessage(error), + }) await sleep(1000) } } diff --git a/apps/sim/lib/organizations/application/member-removal.test.ts b/apps/sim/lib/organizations/application/member-removal.test.ts index ba6816ac84a..f89aa23366b 100644 --- a/apps/sim/lib/organizations/application/member-removal.test.ts +++ b/apps/sim/lib/organizations/application/member-removal.test.ts @@ -56,7 +56,7 @@ const principal = { const input = { organizationId: 'org', userId: 'target' } beforeEach(() => { resetDbChainMock() - mocks.remove.mockResolvedValue({ success: true, billingActions: {} }) + mocks.remove.mockResolvedValue({ success: true, billingActions: {}, stoppedRuns: [] }) mocks.seats.mockResolvedValue({ changed: false }) mocks.external.mockResolvedValue({ success: true, @@ -64,6 +64,7 @@ beforeEach(() => { permissionGroupsRevoked: 1, credentialMembershipsRevoked: 1, pendingInvitationsCancelled: 1, + stoppedRuns: [], }) }) function target(role = 'admin') { @@ -138,7 +139,7 @@ describe('organization member removal', () => { 'Cannot remove the workspace billing account. Please reassign billing first.', ])('preserves lifecycle refusal %s', async (error) => { target() - mocks.remove.mockResolvedValueOnce({ success: false, error }) + mocks.remove.mockResolvedValueOnce({ success: false, error, stoppedRuns: [] }) await expect(removeOrganizationMember.execute({ principal, input })).rejects.toMatchObject({ code: 'validation', }) diff --git a/apps/sim/lib/organizations/application/member-role.test.ts b/apps/sim/lib/organizations/application/member-role.test.ts index 63741aee74f..ea8fea85807 100644 --- a/apps/sim/lib/organizations/application/member-role.test.ts +++ b/apps/sim/lib/organizations/application/member-role.test.ts @@ -54,7 +54,7 @@ const input = { organizationId: 'org', userId: 'target-user', role: 'admin' } as beforeEach(() => { resetDbChainMock() mocks.scim.mockResolvedValue(undefined) - mocks.change.mockResolvedValue({ changed: true, from: 'member', to: 'admin' }) + mocks.change.mockResolvedValue({ changed: true, from: 'member', to: 'admin', stoppedRuns: [] }) }) describe('organization role update', () => { it.each(['owner', 'admin'])( diff --git a/apps/sim/lib/organizations/member-manager.test.ts b/apps/sim/lib/organizations/member-manager.test.ts index 8148d9a7de8..864f7581e2f 100644 --- a/apps/sim/lib/organizations/member-manager.test.ts +++ b/apps/sim/lib/organizations/member-manager.test.ts @@ -51,7 +51,7 @@ const target = { beforeEach(() => { vi.resetAllMocks() resetDbChainMock() - mocks.remove.mockResolvedValue({ success: true }) + mocks.remove.mockResolvedValue({ success: true, stoppedRuns: [] }) mocks.seats.mockResolvedValue({ changed: false }) }) describe('organization member managers', () => { @@ -98,10 +98,38 @@ describe('organization member managers', () => { expect(mocks.scim).not.toHaveBeenCalled() }) + it('never returns the stopped runs’ workflow and sandbox identities to the caller', async () => { + const stoppedRuns = [ + { + id: 'run', + streamId: 'stream', + userId: 'target', + chatId: 'chat', + desktopDeviceId: null, + clientWorkflowExecutionIds: ['execution'], + sandboxProcesses: [], + }, + ] + queueTableRows(member, [target]) + mocks.remove.mockResolvedValue({ success: true, stoppedRuns }) + const internal = await removeOrganizationMemberRecord(input) + expect(internal.removal).not.toHaveProperty('stoppedRuns') + + queueTableRows(member, []) + queueTableRows(user, [{ id: 'target', name: 'Person', email: 'person@example.com' }]) + mocks.external.mockResolvedValue({ success: true, workspaceAccessRevoked: 1, stoppedRuns }) + const external = await removeOrganizationMemberRecord(input) + expect(external.removal).not.toHaveProperty('stoppedRuns') + }) + it('reports membership added before external-removal preflight as a conflict', async () => { queueTableRows(member, []) queueTableRows(user, [{ id: 'target', name: 'Person', email: 'person@example.com' }]) - mocks.external.mockResolvedValue({ success: false, error: 'User is an organization member' }) + mocks.external.mockResolvedValue({ + success: false, + error: 'User is an organization member', + stoppedRuns: [], + }) await expect(removeOrganizationMemberRecord(input)).rejects.toMatchObject({ code: 'conflict', diff --git a/apps/sim/lib/organizations/member-manager.ts b/apps/sim/lib/organizations/member-manager.ts index 7673bc22a48..45e6e2252e6 100644 --- a/apps/sim/lib/organizations/member-manager.ts +++ b/apps/sim/lib/organizations/member-manager.ts @@ -10,6 +10,7 @@ import { } from '@/lib/billing/organizations/membership' import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats' import { OrchestrationError } from '@/lib/core/orchestration/types' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { requireMemberManagementAuthority } from '@/lib/organizations/members/authority' import { changeMemberRoleTx } from '@/lib/organizations/members/lifecycle' import { findOrganizationMemberRecord } from '@/lib/organizations/queries' @@ -23,7 +24,7 @@ export async function updateOrganizationMemberRecord(input: { role: 'member' | 'admin' | 'owner' actorUserId: string }) { - return db.transaction(async (tx) => { + const updated = await db.transaction(async (tx) => { await acquireOrganizationUserMutationLocks(tx, { userId: input.userId, organizationIds: [input.organizationId], @@ -48,8 +49,12 @@ export async function updateOrganizationMemberRecord(input: { member: { ...target, role: input.role }, previousRole: target.role, changed: change.changed, + stoppedRuns: change.changed ? change.stoppedRuns : [], } }) + const { stoppedRuns, ...result } = updated + deliverRunStops(stoppedRuns) + return result } export async function removeOrganizationMemberRecord(input: { @@ -66,7 +71,9 @@ export async function removeOrganizationMemberRecord(input: { .where(eq(user.id, input.userId)) .limit(1) if (!external) throw new OrchestrationError('not_found', 'Member not found') - const removal = await removeExternalUserFromOrganizationWorkspaces(input) + /** The stopped runs are delivered, never returned to the response or audit. */ + const { stoppedRuns, ...removal } = await removeExternalUserFromOrganizationWorkspaces(input) + deliverRunStops(stoppedRuns) if (!removal.success) { const message = removal.error || 'External workspace member not found' const code = @@ -86,11 +93,12 @@ export async function removeOrganizationMemberRecord(input: { seatReduction: null, } } - const removal = await removeUserFromOrganization({ + const { stoppedRuns, ...removal } = await removeUserFromOrganization({ ...input, memberId: target.id, onError: 'throw', }) + deliverRunStops(stoppedRuns) if (!removal.success) { const message = removal.error || 'Failed to remove user from organization' const code = diff --git a/apps/sim/lib/organizations/members/lifecycle.ts b/apps/sim/lib/organizations/members/lifecycle.ts index 57ed51cbb85..af7fe445c35 100644 --- a/apps/sim/lib/organizations/members/lifecycle.ts +++ b/apps/sim/lib/organizations/members/lifecycle.ts @@ -1,10 +1,13 @@ -import { member, user } from '@sim/db/schema' +import { member, user, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { isOrgAdminRole } from '@sim/platform-authz/workspace' import { and, eq, isNull } from 'drizzle-orm' import { acquireOrganizationUserMutationLocks } from '@/lib/billing/organizations/membership' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbOrTx, DbTransaction } from '@/lib/db/types' +import type { StoppedRun } from '@/lib/mothership/async-runs/repository' import { revokeUserSessionsTx } from '@/lib/organizations/members/revocation' +import { stopRunsWithoutWorkspaceAccessTx } from '@/lib/workspaces/access/workspace-access' const logger = createLogger('OrganizationMemberLifecycle') @@ -89,7 +92,13 @@ export async function unsuspendMemberTx( export type OrganizationMemberRole = 'admin' | 'member' export type ChangeMemberRoleResult = - | { changed: true; from: string; to: OrganizationMemberRole } + | { + changed: true + from: string + to: OrganizationMemberRole + /** Runs a demotion stopped where it ended the member's derived workspace access. */ + stoppedRuns: StoppedRun[] + } | { changed: false; role: string } /** @@ -101,7 +110,16 @@ export type ChangeMemberRoleResult = */ export async function changeMemberRoleTx( tx: DbTransaction, - params: { organizationId: string; userId: string; role: OrganizationMemberRole } + params: { + organizationId: string + userId: string + role: OrganizationMemberRole + /** + * Leaves the member's runs to a caller that changes more access in this transaction and + * stops runs once that access is final; `stoppedRuns` is then empty. + */ + deferRunStops?: boolean + } ): Promise { await acquireOrganizationUserMutationLocks(tx, { userId: params.userId, @@ -121,11 +139,25 @@ export async function changeMemberRoleTx( if (current.role === params.role) return { changed: false, role: current.role } await tx.update(member).set({ role: params.role }).where(eq(member.id, current.id)) + /** An organization admin holds derived access to every organization workspace; a demotion ends it. */ + const stoppedRuns = + !params.deferRunStops && isOrgAdminRole(current.role) && !isOrgAdminRole(params.role) + ? await stopRunsWithoutWorkspaceAccessTx( + tx, + params.userId, + ( + await tx + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.organizationId, params.organizationId)) + ).map(({ id }) => id) + ) + : [] logger.info('Changed organization member role', { organizationId: params.organizationId, userId: params.userId, from: current.role, to: params.role, }) - return { changed: true, from: current.role, to: params.role } + return { changed: true, from: current.role, to: params.role, stoppedRuns } } diff --git a/apps/sim/lib/projects/application/authorization.ts b/apps/sim/lib/projects/application/authorization.ts index 75fe2291a15..f91e22c75a5 100644 --- a/apps/sim/lib/projects/application/authorization.ts +++ b/apps/sim/lib/projects/application/authorization.ts @@ -192,16 +192,17 @@ function projectVisibility( * Loads and holds canonical Project membership for a resource-specific access policy. `lock` is * the mode the Project is held in: `shared` for a caller that only needs it fenced against * lifecycle and payer changes, `exclusive` for one that will take it exclusively later in the - * transaction, which must never upgrade a shared hold. + * transaction, which must never upgrade a shared hold. `snapshot` holds nothing and relies on + * the caller's read-only snapshot, for a caller that acts on nothing in the transaction. */ export async function loadProjectAccess( tx: DbTransaction, userId: string, input: ProjectAuthorizationInput & { projectId: string }, - lock: 'shared' | 'exclusive' = 'exclusive' + lock: 'shared' | 'exclusive' | 'snapshot' = 'exclusive' ) { if (lock === 'shared') await lockProjectShared(tx, input.projectId) - else await lockProject(tx, input.projectId) + else if (lock === 'exclusive') await lockProject(tx, input.projectId) const [record] = await tx.select().from(project).where(eq(project.id, input.projectId)).limit(1) if ( !record || @@ -210,7 +211,7 @@ export async function loadProjectAccess( throw new OrchestrationError('not_found', 'Project not found') return projectVisibility( record, - await loadProjectMemberships(tx, userId, [record], 'hold'), + await loadProjectMemberships(tx, userId, [record], lock === 'snapshot' ? 'snapshot' : 'hold'), input ) } diff --git a/apps/sim/lib/projects/application/use-cases.ts b/apps/sim/lib/projects/application/use-cases.ts index 871fc3fce59..cafe0777f37 100644 --- a/apps/sim/lib/projects/application/use-cases.ts +++ b/apps/sim/lib/projects/application/use-cases.ts @@ -7,6 +7,7 @@ import { eq, inArray, sql } from 'drizzle-orm' import { recordProjectedUseCaseAuditEntries } from '@/lib/core/application/authorized-workspace-use-case' import type { OperationUseCase } from '@/lib/core/application/operation' import { OrchestrationError } from '@/lib/core/orchestration/types' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { type AuthorizedProject, authorizeProject, @@ -209,6 +210,7 @@ export const archiveProject: OperationUseCase< ], result.context.record.organizationId ?? undefined ) + deliverRunStops(result.effects.flatMap((environment) => environment.stoppedRuns)) await finishProjectArchive(result.effects, `project:${input.projectId}`) return { id: input.projectId, archived: true } }, diff --git a/apps/sim/lib/projects/backfill-repair.ts b/apps/sim/lib/projects/backfill-repair.ts index cd4ea4ad3b8..e090ae03c13 100644 --- a/apps/sim/lib/projects/backfill-repair.ts +++ b/apps/sim/lib/projects/backfill-repair.ts @@ -117,7 +117,11 @@ export async function repairArchivedProjectEnvironment( archivedAt ) } - await archiveEnvironmentInTransaction(tx, repair.workspaceId, archivedAt) + /** + * Its Stop for live runs commits here; this operator script stays out of the delivery's + * module graph, so run control ends those runs on their next heartbeat. + */ + const archived = await archiveEnvironmentInTransaction(tx, repair.workspaceId, archivedAt) const tools = await tx .select({ workflowId: schema.workflowMcpTool.workflowId, @@ -130,7 +134,7 @@ export async function repairArchivedProjectEnvironment( .from(schema.workflowMcpServer) .where(eq(schema.workflowMcpServer.workspaceId, repair.workspaceId)) const result: EnvironmentArchiveEffects = { - workspaceId: repair.workspaceId, + ...archived, workflows: repair.workflowIds.map((id) => ({ id, serverIds: tools.filter((tool) => tool.workflowId === id).map((tool) => tool.serverId), diff --git a/apps/sim/lib/projects/files/__integration__/content-bulk.integration.ts b/apps/sim/lib/projects/files/__integration__/content-bulk.integration.ts index 1ece5a545e8..9f2f940e04e 100644 --- a/apps/sim/lib/projects/files/__integration__/content-bulk.integration.ts +++ b/apps/sim/lib/projects/files/__integration__/content-bulk.integration.ts @@ -36,6 +36,8 @@ vi.mock('@/lib/uploads/core/setup.server', () => uploadsSetupMock) vi.mock('@/lib/messaging/email/mailer', () => emailMailerMock) import { observeContentDelivery } from '@/lib/core/application/content-delivery' +import { encryptSecret } from '@/lib/core/security/encryption' +import { createDurableSecretProvenanceRegistry } from '@/lib/execution/durable-secret-provenance' import { createProjectFile, readProjectFileContent, @@ -45,6 +47,7 @@ import { createProjectFileFolder } from '@/lib/projects/files/application/folder import type { WorkspaceFileSecretProvenance } from '@/lib/uploads/contexts/workspace/file-secret-provenance' import * as storage from '@/lib/uploads/core/storage-service' import { observeWorkspaceFileDelivery } from '@/lib/workspace-files/application/file-delivery-observer' +import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) @@ -1414,7 +1417,7 @@ describe('Project ZIP extraction against PostgreSQL and local storage', () => { status: 'exact' as const, entries: [ { - encryptedValue: 'archive-fixture', + encryptedValue: (await encryptSecret('extracted-secret-value')).encrypted, sourceUserId: 'origin', }, ], @@ -1422,7 +1425,7 @@ describe('Project ZIP extraction against PostgreSQL and local storage', () => { ] for (const classification of classifications) { const f = await fixture() - const source = await archive(f, { 'data.txt': 'extracted' }, classification) + const source = await archive(f, { 'data.txt': 'extracted-secret-value' }, classification) await extractProjectFile.execute({ principal: f.principal, input: { projectId: f.projectId, fileId: source.file.id }, @@ -1433,15 +1436,23 @@ describe('Project ZIP extraction against PostgreSQL and local storage', () => { .select() .from(workspaceFileSecretProvenance) .where(eq(workspaceFileSecretProvenance.fileId, child.id)) - const expected = - classification.status === 'exact' - ? classification.entries.length - ? 'unknown' - : 'exact' - : classification.status - expect(provenance.status).toBe(expected) - expect(provenance.entries).toEqual([]) - expect(await readFile(join(localStorageRoot, child.key), 'utf8')).toBe('extracted') + expect(provenance.status).toBe(classification.status) + const read = await readProjectFileContent.execute({ + principal: f.principal, + input: { projectId: f.projectId, fileId: child.id, includeSecretProvenance: true }, + }) + expect(read.content.toString()).toBe('extracted-secret-value') + if (classification.status === 'exact' && classification.entries.length > 0) { + if (read.secretProvenance?.status !== 'exact') + throw new Error('Child lost its recorded evidence') + const registry = await createDurableSecretProvenanceRegistry(read.secretProvenance, { + userId: f.ownerId, + workspaceId: f.workspaceId, + }) + expect( + projectResolvedSecretModelContent(read.content.toString(), registry) + ).toMatchObject({ safe: true, value: '[REDACTED_SECRET]' }) + } else expect(provenance.entries).toEqual([]) } } ) diff --git a/apps/sim/lib/projects/files/application/authorization.ts b/apps/sim/lib/projects/files/application/authorization.ts index b180ec2fd35..3c2986c6554 100644 --- a/apps/sim/lib/projects/files/application/authorization.ts +++ b/apps/sim/lib/projects/files/application/authorization.ts @@ -1,4 +1,9 @@ -import { type Principal, requirePrincipalSubjectUserId } from '@sim/auth/principal' +import { + type OrganizationDelegatedPrincipal, + type Principal, + requirePrincipalSubjectUserId, + type SubjectDelegatedPrincipal, +} from '@sim/auth/principal' import { copilotChats, permissions, @@ -93,19 +98,26 @@ function requirePrincipal( } } -/** Rechecks canonical conversation ownership and authoring capability while authorization locks are held. */ +/** + * Rechecks canonical conversation ownership and authoring capability while authorization locks + * are held; `snapshot` reads the same rows unlocked inside the caller's read-only snapshot. + */ export async function requireCurrentCopilotProjectInvocation( tx: DbTransaction, - principal: Extract, + principal: Pick< + Extract, + 'subjectUserId' | 'invocation' + >, access: Awaited>, - scope: Awaited> + scope: Awaited>, + mode: 'hold' | 'snapshot' = 'hold' ) { if (scope.organizationId !== access.record.organizationId) { throw new OrchestrationError('not_found', 'Project not found in this conversation') } let workspaceId: string | null if (principal.invocation.kind === 'chat') { - const [chat] = await tx + const chatQuery = tx .select({ userId: copilotChats.userId, workspaceId: copilotChats.workspaceId, @@ -115,8 +127,8 @@ export async function requireCurrentCopilotProjectInvocation( }) .from(copilotChats) .where(and(eq(copilotChats.id, principal.invocation.chatId), isNull(copilotChats.deletedAt))) - .for('share') .limit(1) + const [chat] = await (mode === 'hold' ? chatQuery.for('share') : chatQuery) if ( !chat || chat.userId !== principal.subjectUserId || @@ -150,7 +162,7 @@ export async function requireCurrentCopilotProjectInvocation( if (!workspaceId || workspaceId !== scope.workspaceId) { throw new OrchestrationError('not_found', 'Project not found in this conversation') } - const [origin] = await tx + const originQuery = tx .select({ workspaceId: workspace.id, workspaceOrganizationId: workspace.organizationId, @@ -158,22 +170,24 @@ export async function requireCurrentCopilotProjectInvocation( }) .from(workspace) .where(and(eq(workspace.id, workspaceId), isNull(workspace.archivedAt))) - .for('share') .limit(1) + const [origin] = await (mode === 'hold' ? originQuery.for('share') : originQuery) if (!origin || origin.workspaceOrganizationId !== scope.organizationId) throw new OrchestrationError('not_found', 'Workspace not found in this conversation') - // Hold origin grants without upgrading the target's existing SHARE locks. - await tx - .select({ id: permissions.id }) - .from(permissions) - .where( - and( - eq(permissions.userId, principal.subjectUserId), - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workspaceId) + if (mode === 'hold') { + // Hold origin grants without upgrading the target's existing SHARE locks. + await tx + .select({ id: permissions.id }) + .from(permissions) + .where( + and( + eq(permissions.userId, principal.subjectUserId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) ) - ) - .for('share') + .for('share') + } await requireCurrentHumanRole(principal.subjectUserId, origin, 'read', { executor: tx }) await assertWorkspaceCapability( principal.subjectUserId, @@ -187,12 +201,13 @@ export async function requireCurrentCopilotProjectInvocation( /** Shared owner policy keeps compound copies subject to the same live Project edit rule. */ export async function requireProjectFileOwnerRole( tx: DbTransaction, - principal: ProjectFilePrincipal, + principal: ProjectFilePrincipal | SubjectDelegatedPrincipal | OrganizationDelegatedPrincipal, access: Awaited>, - accessMode: 'read' | 'write' + accessMode: 'read' | 'write', + mode: 'hold' | 'snapshot' = 'hold' ): Promise { if (access.record.archivedAt) throw new OrchestrationError('conflict', 'Project is archived') - await requireCurrentFileSubject(tx, principal) + await requireCurrentFileSubject(tx, principal, mode) const canWrite = access.orgAdmin || access.active.some((row) => row.permission === 'admin') || @@ -217,7 +232,7 @@ export async function requireProjectFileOwnerRole( /** Credential and Files capability checks apply to every currently visible environment. */ export async function requireProjectFileOwnerCapabilities( tx: DbTransaction, - principal: ProjectFilePrincipal, + principal: ProjectFilePrincipal | SubjectDelegatedPrincipal | OrganizationDelegatedPrincipal, access: Awaited>, capability: ProjectFileOperation['capability'] ): Promise { diff --git a/apps/sim/lib/public-shares/api/index.ts b/apps/sim/lib/public-shares/api/index.ts index d1f19dddef0..d2c4112631f 100644 --- a/apps/sim/lib/public-shares/api/index.ts +++ b/apps/sim/lib/public-shares/api/index.ts @@ -1,5 +1,6 @@ export { publicFileShareCredential } from './credentials' export { + logPublicFileError, publicFileAuthDenied, publicFileBinaryErrorResponse, publicFileErrorResponse, diff --git a/apps/sim/lib/public-shares/api/responses.ts b/apps/sim/lib/public-shares/api/responses.ts index e47a7cd2e4f..e5432b741a7 100644 --- a/apps/sim/lib/public-shares/api/responses.ts +++ b/apps/sim/lib/public-shares/api/responses.ts @@ -1,3 +1,4 @@ +import type { Logger } from '@sim/logger' import { NextResponse } from 'next/server' import { asOrchestrationError, @@ -39,6 +40,12 @@ function classifyPublicFileError(error: unknown, fallback: string) { } } +/** A classified client failure, such as a missing share or the wrong auth mode, is only a warning. */ +export function logPublicFileError(logger: Logger, message: string, error: unknown) { + if (classifyPublicFileError(error, '').status < 500) logger.warn(message, error) + else logger.error(message, error) +} + /** Conceal unavailable targets and driver failures while preserving classified domain failures. */ export function publicFileErrorResponse(error: unknown, fallback: string) { const result = classifyPublicFileError(error, fallback) diff --git a/apps/sim/lib/public-shares/application/read.ts b/apps/sim/lib/public-shares/application/read.ts index 9ff4ef28c7a..641c227981e 100644 --- a/apps/sim/lib/public-shares/application/read.ts +++ b/apps/sim/lib/public-shares/application/read.ts @@ -37,7 +37,11 @@ import { resolveEffectiveMimeType } from '@/lib/uploads/utils/file' import { sniffImageContentType } from '@/lib/uploads/utils/validation' import { reportWorkspaceFileDelivery } from '@/lib/workspace-files/application/file-delivery-observer' import { fileOwnerCondition } from '@/lib/workspace-files/ownership-query' -import { isSimPageSource, SIM_PAGE_CONTENT_TYPE } from '@/lib/workspace-files/page-compile' +import { + isSimPageSource, + ProjectPageForeignEmbedError, + SIM_PAGE_CONTENT_TYPE, +} from '@/lib/workspace-files/page-compile' import { renderSimPageDocument } from '@/lib/workspace-files/page-document' import { collectSimPageFileReferences, @@ -70,6 +74,16 @@ function present(snapshot: PublicFileShareSnapshot) { }, } } +/** A Project page embedding another owner's file reads as a missing input, as for members. */ +function renderSharedPage(source: string, snapshot: PublicFileShareSnapshot) { + try { + return renderSimPageDocument(source, snapshot.adapter.pageOptions(snapshot.owner.entityId)) + } catch (error) { + if (error instanceof ProjectPageForeignEmbedError) + throw new OrchestrationError('not_found', 'Document input not found') + throw error + } +} function requireRevision(current: WorkspaceFileRow, expected: WorkspaceFileRow) { if ( current.id !== expected.id || @@ -287,7 +301,7 @@ export async function readPublicFileShareContent({ (snapshot.file.contentType === SIM_PAGE_CONTENT_TYPE || snapshot.file.originalName.toLowerCase().endsWith('.html')) && isSimPageSource(source) - ? renderSimPageDocument(source, snapshot.adapter.pageOptions(snapshot.owner.entityId)) + ? renderSharedPage(source, snapshot) : undefined const references = pageHtml ? collectSimPageFileReferences(pageHtml) : [] if ( @@ -386,9 +400,7 @@ export async function readPublicFileShareInline({ operation, async (tx, snapshot): Promise => { requireRevision(snapshot.file, initial.file) - const pageHtml = isSimPageSource(source) - ? renderSimPageDocument(source, snapshot.adapter.pageOptions(snapshot.owner.entityId)) - : undefined + const pageHtml = isSimPageSource(source) ? renderSharedPage(source, snapshot) : undefined const reference = fileId ? { fileId } : key ? { key } : null const references = pageHtml ? collectSimPageFileReferences(pageHtml) : [] const embedded = diff --git a/apps/sim/lib/sim-search/live/atlassian.test.ts b/apps/sim/lib/sim-search/live/atlassian.test.ts index 7a6fe15f8a3..13d36f8d857 100644 --- a/apps/sim/lib/sim-search/live/atlassian.test.ts +++ b/apps/sim/lib/sim-search/live/atlassian.test.ts @@ -331,3 +331,175 @@ describe('Confluence plain search ranking', () => { expect(own.nextCursor).toBe('page-2') }) }) + +describe('Jira plain search ranking', () => { + const search = '/ex/jira/cloud/rest/api/3/search/jql' + type Issue = string | { key: string; summary: string } + /** + * Answers the all-terms query with `exact`, the all-terms summary query with `titled`, and the + * half-the-terms summary query with `half`, or their failures. + */ + function jira( + exact: { ids: string[]; more: boolean }, + titled: Issue[] | (() => never), + half: Issue[] | (() => never) = [] + ) { + const toRow = (row: Issue) => (typeof row === 'string' ? { key: row, summary: row } : row) + const issues = (rows: Issue[]) => + rows.map(toRow).map(({ key, summary }) => ({ key, fields: { summary } })) + const json = vi.fn( + async (path: string, options?: { body?: { jql?: string; maxResults?: number } }) => { + if (path === '/oauth/token/accessible-resources') return [SITE] + if (path !== search) throw new Error(`Unexpected request: ${path}`) + const jql = options?.body?.jql ?? '' + if (jql.includes('summary ~')) { + const rows = jql.includes(' OR ') ? half : titled + const picked = typeof rows === 'function' ? rows() : rows + return { issues: issues(picked).slice(0, options?.body?.maxResults) } + } + return { issues: issues(exact.ids), ...(exact.more ? { nextPageToken: 'page-2' } : {}) } + } + ) + return { json, text: vi.fn(), bytes: vi.fn() } satisfies NativeClient + } + const query = { query: 'merged results pipelines', limit: 3, scopes: [] } + const overflowing = { ids: ['NEW-3', 'NEW-2', 'NEW-1'], more: true } + + it('leads an all-terms match too large for one page with issues whose summary holds every term', async () => { + const api = jira(overflowing, ['TOP-1', 'NEW-2']) + const page = await searchAtlassian(api, 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['TOP-1', 'NEW-2', 'NEW-3']) + expect(page.hasMore).toBe(true) + expect(page.partial).toBe(true) + expect(page.nextCursor).toBeUndefined() + }) + + it('keeps the all-terms page and its cursor when no summary holds every term', async () => { + const page = await searchAtlassian(jira(overflowing, []), 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['NEW-3', 'NEW-2', 'NEW-1']) + expect(page.nextCursor).toBe('page-2') + expect(page.partial).toBe(false) + }) + + it.each([ + ['a provider refusal', new NativeSearchError('rate_limited', 'Rate limited.', 30)], + ['a request timeout', new Error('Request timed out after 10000ms')], + ])( + 'keeps the all-terms page and its cursor when the summary search fails on %s', + async (_, failure) => { + const api = jira(overflowing, () => { + throw failure + }) + const page = await searchAtlassian(api, 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['NEW-3', 'NEW-2', 'NEW-1']) + expect(page.nextCursor).toBe('page-2') + } + ) + + it('propagates a cancellation during the summary search', async () => { + const caller = new AbortController() + const reason = new DOMException('Search cancelled', 'AbortError') + const api = jira(overflowing, () => { + caller.abort(reason) + throw reason + }) + await expect(searchAtlassian(api, 'jira', { ...query, signal: caller.signal })).rejects.toBe( + reason + ) + }) + + it('keeps an all-terms match that fits on one page without a summary search', async () => { + const api = jira({ ids: ['ONLY-1'], more: false }, ['TOP-1']) + const page = await searchAtlassian(api, 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['ONLY-1']) + expect(api.json).toHaveBeenCalledTimes(2) + }) + + it('keeps date order, native queries and any-term matches on their own page', async () => { + for (const input of [ + { ...query, filters: { sortBy: 'newest' as const } }, + { ...query, native: { provider: 'jira' as const, query: 'text ~ "merged results"' } }, + { ...query, textMatch: 'any' as const }, + ]) { + const api = jira({ ids: ['NEW-3', 'NEW-2'], more: true }, ['TOP-1']) + const page = await searchAtlassian(api, 'jira', input) + expect(page.documents.map((document) => document.id)).not.toContain('TOP-1') + expect(api.json).toHaveBeenCalledTimes(2) + } + }) + + it('leads with issues whose summary holds at least half the terms when none holds them all', async () => { + const api = jira( + overflowing, + [], + [ + { key: 'LOOSE-1', summary: 'Pipelines dashboard' }, + { key: 'HALF-1', summary: 'Merged results broke the dashboard' }, + ] + ) + const page = await searchAtlassian(api, 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['HALF-1', 'NEW-3', 'NEW-2']) + expect(page.nextCursor).toBeUndefined() + }) + + it('keeps a two-term query to summaries holding every term', async () => { + const api = jira(overflowing, [], [{ key: 'HALF-1', summary: 'Merged pipelines' }]) + const page = await searchAtlassian(api, 'jira', { ...query, query: 'merged pipelines' }) + expect(page.documents.map((document) => document.id)).toEqual(['NEW-3', 'NEW-2', 'NEW-1']) + expect(api.json).toHaveBeenCalledTimes(3) + }) + + it('keeps the all-terms page and its cursor when the half-the-terms summary search fails', async () => { + const api = jira(overflowing, [], () => { + throw new NativeSearchError('rate_limited', 'Rate limited.', 30) + }) + const page = await searchAtlassian(api, 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['NEW-3', 'NEW-2', 'NEW-1']) + expect(page.nextCursor).toBe('page-2') + }) + + it('counts a word and its plural as one term before allowing the half-the-terms fallback', async () => { + const api = jira(overflowing, [], [{ key: 'ONE-1', summary: 'Rotation schedule' }]) + const page = await searchAtlassian(api, 'jira', { ...query, query: 'token tokens rotation' }) + expect(page.documents.map((document) => document.id)).toEqual(['NEW-3', 'NEW-2', 'NEW-1']) + }) + + it('orders half-the-terms summaries by how many terms they hold', async () => { + const api = jira( + overflowing, + [], + [ + { key: 'TWO-1', summary: 'Merged results dashboard' }, + { key: 'THREE-1', summary: 'Merged results in release pipelines' }, + ] + ) + const page = await searchAtlassian(api, 'jira', { + ...query, + query: 'merged results release pipelines', + }) + expect(page.documents.map((document) => document.id)).toEqual(['THREE-1', 'TWO-1', 'NEW-3']) + }) + + it('finds half-the-terms summaries beyond the first page of pair matches', async () => { + const pairs = Array.from({ length: 10 }, (_, index) => ({ + key: `PAIR-${index}`, + summary: 'Pipelines dashboard', + })) + const api = jira(overflowing, [], [...pairs, { key: 'HALF-9', summary: 'Merged pipelines' }]) + const page = await searchAtlassian(api, 'jira', query) + expect(page.documents.map((document) => document.id)[0]).toBe('HALF-9') + }) + + it('follows summaries holding every term with those holding half of them', async () => { + const api = jira( + { ids: ['NEW-3', 'NEW-2', 'NEW-1'], more: true }, + ['ALL-1'], + [ + { key: 'ALL-1', summary: 'Merged results pipelines' }, + { key: 'HALF-1', summary: 'Merged results dashboard' }, + ] + ) + const page = await searchAtlassian(api, 'jira', query) + expect(page.documents.map((document) => document.id)).toEqual(['ALL-1', 'HALF-1', 'NEW-3']) + }) +}) diff --git a/apps/sim/lib/sim-search/live/atlassian.ts b/apps/sim/lib/sim-search/live/atlassian.ts index 4283b3eddf2..b355b316c83 100644 --- a/apps/sim/lib/sim-search/live/atlassian.ts +++ b/apps/sim/lib/sim-search/live/atlassian.ts @@ -13,6 +13,7 @@ import { confluenceTextClause, holdsHalfTheTerms, jiraTextClause, + stemmedTerms, } from '@/lib/sim-search/live/text-match' import type { NativeClient, @@ -116,6 +117,13 @@ async function sites(client: NativeClient) { return array(await client.json('/oauth/token/accessible-resources', { memo: true })) } +/** + * Pair matches fetched for the half-the-terms summary fallback. Jira returns them newest first, + * not by relevance, and the page is then filtered to summaries holding enough terms, so it starts + * from a wider pool than the caller's limit or the any-term fallback's. + */ +const HALF_TERM_CANDIDATES = 50 + /** * One Atlassian site's page; `ranking` marks a page whose order or completeness differs from the * all-terms search. @@ -175,33 +183,87 @@ export async function searchAtlassian( const cloudId = string(site.id) const origin = string(site.url).replace(/\/$/, '') if (provider === 'jira') { - const data = object( - await client.json(`/ex/jira/${segment(cloudId)}/rest/api/3/search/jql`, { - body: { - jql: scopeAtlassianQuery( - input.native?.query || (text ? jiraTextClause(text, input.textMatch) : ''), - scope, - order - ), - maxResults: input.limit, - fields: ['summary', 'description', 'updated', 'creator', 'status'], - ...(input.native?.cursor && single ? { nextPageToken: input.native.cursor } : {}), - }, - signal: input.signal, - }) + /** One JQL page, each issue with its summary and raw fields. */ + const searchIssues = async ( + jql: string, + { cursor, maxResults = input.limit }: { cursor?: string; maxResults?: number } = {} + ) => { + const data = object( + await client.json(`/ex/jira/${segment(cloudId)}/rest/api/3/search/jql`, { + body: { + jql: scopeAtlassianQuery(jql, scope, order), + maxResults, + fields: ['summary', 'description', 'updated', 'creator', 'status'], + ...(cursor ? { nextPageToken: cursor } : {}), + }, + signal: input.signal, + }) + ) + return { + issues: array(data.issues).map((row) => { + const fields = object(row.fields) + return { + document: issue(row, cloudId, origin), + summary: string(fields.summary), + fields, + } + }), + next: string(data.nextPageToken) || undefined, + } + } + const exact = await searchIssues( + input.native?.query || (text ? jiraTextClause(text, input.textMatch) : ''), + { cursor: input.native?.cursor && single ? input.native.cursor : undefined } ) + const exactPage: SitePage = { + documents: exact.issues + .filter( + ({ summary, fields }) => + !relevant || relevant(`${summary}\n${textContent(fields.description)}`) + ) + .map(({ document }) => document), + excluded: false, + next: exact.next, + } + // JQL has no relevance order, so an all-terms match larger than a page comes back newest + // first; issues whose summary holds every term lead it instead, then those holding at + // least half of three or more terms (most terms first). + if (!text || input.native || order || input.textMatch === 'any' || !exact.next) + return exactPage + const terms = stemmedTerms(text) + const [every, half] = await Promise.allSettled([ + searchIssues(jiraTextClause(text, 'all', 'summary')), + terms.count >= 3 + ? searchIssues(jiraTextClause(text, 'any', 'summary'), { + maxResults: HALF_TERM_CANDIDATES, + }) + : undefined, + ]) + // A cancelled search stops here; any other failure keeps the all-terms page. + input.signal?.throwIfAborted() + const everyTerm = + every.status === 'fulfilled' ? every.value.issues.map(({ document }) => document) : [] + const halfTerms = + half.status === 'fulfilled' && half.value + ? half.value.issues + .map(({ document, summary }) => ({ document, held: terms.held(summary) })) + .filter(({ held }) => held >= terms.required) + .sort((left, right) => right.held - left.held) + .map(({ document }) => document) + : [] + const titled = [...everyTerm, ...halfTerms] + if (!titled.length) return exactPage + const seen = new Set() return { - documents: array(data.issues) - .filter((row) => { - const fields = object(row.fields) - return ( - !relevant || - relevant(`${string(fields.summary)}\n${textContent(fields.description)}`) - ) + documents: [...titled, ...exactPage.documents] + .filter(({ id }) => { + if (seen.has(id)) return false + seen.add(id) + return true }) - .map((row) => issue(row, cloudId, origin)), + .slice(0, input.limit), excluded: false, - next: string(data.nextPageToken) || undefined, + ranking: 'relevance', } } const searchPages = async (clause: string, cursor?: string) => { @@ -265,7 +327,9 @@ export async function searchAtlassian( ? ' Matches other than pages, blog posts, and spaces (attachments, comments, whiteboards, folders, databases) were excluded because they cannot be read.' : '') + (pages.some((result) => result.ranking === 'relevance') - ? ' More pages contained every word than fit on one page, so this page is ordered by Confluence relevance search; refine the query, or page with a native CQL query.' + ? provider === 'jira' + ? ' More issues contained every word than fit on one page, so issues whose summary holds every word, then most of them, come first; refine the query, or page with a native JQL query.' + : ' More pages contained every word than fit on one page, so this page is ordered by Confluence relevance search; refine the query, or page with a native CQL query.' : '') + (pages.some((result) => result.ranking === 'unavailable') ? ' Relevance ranking was unavailable, so this page keeps the order of the all-terms search.' diff --git a/apps/sim/lib/sim-search/live/text-match.ts b/apps/sim/lib/sim-search/live/text-match.ts index 2f5ed21fbdf..9f3f1bfa237 100644 --- a/apps/sim/lib/sim-search/live/text-match.ts +++ b/apps/sim/lib/sim-search/live/text-match.ts @@ -89,14 +89,19 @@ const escapeSearchPhrase = (value: string) => JSON.stringify(value.replace(/[+\-&|!(){}[\]^"~*?:\\/]/g, '\\$&')) /** - * The `text ~` clause for a plain query in Jira JQL. `any` ORs every pair of the query's longest - * terms, so a match contains at least two of them somewhere in the issue, or either term of a - * two-term query. Jira stems text search, so a word and its plural share one term. + * The clause for a plain query in Jira JQL, over all text fields or the summary alone. `any` ORs + * every pair of the query's longest terms, so a match contains at least two of them in that field, + * or either term of a two-term query. Jira stems text search, so a word and its plural share one + * term. */ -export function jiraTextClause(text: string, match: TextMatch = 'all'): string { +export function jiraTextClause( + text: string, + match: TextMatch = 'all', + field: 'text' | 'summary' = 'text' +): string { const terms = match === 'any' ? distinctTerms(text, singularKey) : [] - if (terms.length < 2) return `text ~ ${escapeSearchPhrase(text)}` - const clause = (term: string) => `text ~ ${escapeSearchPhrase(term)}` + if (terms.length < 2) return `${field} ~ ${escapeSearchPhrase(text)}` + const clause = (term: string) => `${field} ~ ${escapeSearchPhrase(term)}` const groups = terms.length === 2 ? terms.map(clause) @@ -121,19 +126,35 @@ export function termCoverage(text: string, terms: readonly string[]): number { } /** - * A test for whether a text holds at least half the query's distinct terms. Query and text words - * are compared by their singular key, so a word and its regular plural count as one term as a - * stemmed search matches them; a term with punctuation inside it is matched as written. + * The query's distinct terms as a stemmed search counts them, and how many of them a text holds. + * Query and text words are compared by their singular key, so a word and its regular plural count + * as one term; a term with punctuation inside it is matched as written. */ -export function holdsHalfTheTerms(query: string): (text: string) => boolean { +export function stemmedTerms(query: string): { + count: number + /** At least half the terms, the minimum a partial match must hold. */ + required: number + held: (text: string) => number +} { const terms = distinctTerms(query, singularKey) const keys = terms.map(singularKey) - const required = Math.ceil(terms.length / 2) - return (text) => { - const words = new Set(textWords(text).map(singularKey)) - const present = terms.filter( - (term, index) => words.has(keys[index]) || findTermMatches(text, [term]).length > 0 - ) - return present.length >= required + return { + count: terms.length, + required: Math.ceil(terms.length / 2), + held: (text) => { + const words = new Set(textWords(text).map(singularKey)) + return terms.filter( + (term, index) => words.has(keys[index]) || findTermMatches(text, [term]).length > 0 + ).length + }, } } + +/** + * A test for whether a text holds at least half the query's distinct terms, as `stemmedTerms` + * counts them. + */ +export function holdsHalfTheTerms(query: string): (text: string) => boolean { + const { required, held } = stemmedTerms(query) + return (text) => held(text) >= required +} diff --git a/apps/sim/lib/slack-search/shared-app.ts b/apps/sim/lib/slack-search/shared-app.ts index 9c7ba042415..5a36842d825 100644 --- a/apps/sim/lib/slack-search/shared-app.ts +++ b/apps/sim/lib/slack-search/shared-app.ts @@ -4,6 +4,7 @@ import { and, eq } from 'drizzle-orm' import { isHosted } from '@/lib/core/config/env-flags' import { isFeatureEnabled } from '@/lib/core/config/feature-flags' import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { DbOrTx } from '@/lib/db/types' import { getSharedSlackSearchAppConfiguration } from '@/lib/slack-search/shared-app-env' /** Called only inside authorized installation/member operations; never returns secrets to a surface. */ @@ -14,14 +15,18 @@ export async function readSharedSlackSearchApp(organizationId: string) { } /** Existing custom bots remain independent of the shared-app rollout. */ -export async function requireSlackSearchAppAvailable(appId: string, organizationId: string) { +export async function requireSlackSearchAppAvailable( + appId: string, + organizationId: string, + executor: DbOrTx = db +) { const shared = getSharedSlackSearchAppConfiguration(appId) if (shared?.id === appId) { if (!(await readSharedSlackSearchApp(organizationId))) throw new OrchestrationError('forbidden', 'The shared Slack Search app is unavailable') return } - const [app] = await db + const [app] = await executor .select({ kind: slackApp.kind }) .from(slackApp) .where(eq(slackApp.id, appId)) diff --git a/apps/sim/lib/table/application/rows.ts b/apps/sim/lib/table/application/rows.ts index b10607ebbbc..948076cc1ee 100644 --- a/apps/sim/lib/table/application/rows.ts +++ b/apps/sim/lib/table/application/rows.ts @@ -654,7 +654,7 @@ export const queryTableRows = defineAuthorizedTableUseCase({ rowCount: isCopilotPrincipal(principal) ? rows.length : result.rowCount, rows: rowsForReader(principal, rows), ...(input.includePersistedSecretProvenance && readProvenance - ? { secretProvenance: readProvenance.exportProvenance() } + ? { secretProvenance: readProvenance.exportProvenance(rows.map((row) => row.id)) } : {}), } } catch (error) { diff --git a/apps/sim/lib/table/rows/secret-provenance.ts b/apps/sim/lib/table/rows/secret-provenance.ts index 1d49c72e252..2f2153a017c 100644 --- a/apps/sim/lib/table/rows/secret-provenance.ts +++ b/apps/sim/lib/table/rows/secret-provenance.ts @@ -988,8 +988,9 @@ function envelopeFromClassifications( rowCount: number, scope: ResolvedSecretTraceScopeV1 ): ResolvedSecretTraceProvenanceV1 { + /** A tracked row whose sidecar is stale, missing or malformed reads as unknown by policy; writer faults log where they write. */ const incomplete = (cause: string): ResolvedSecretTraceProvenanceV1 => { - logger.error('Table row read could not establish secret provenance', { + logger.warn('Table row read could not establish secret provenance', { surface: 'table-row', cause, rowCount, @@ -1028,7 +1029,7 @@ function envelopeFromClassifications( export class TableRowProvenanceReader { private readonly accumulator: ResolvedSecretTraceProvenanceAccumulator /** Every row captured, classified on its own. A row never captured has no classification. */ - private readonly rows = new Map() + private readonly rows = new Map() constructor( private readonly scope: ResolvedSecretTraceScopeV1, @@ -1060,25 +1061,28 @@ export class TableRowProvenanceReader { executor ) for (const [rowId, classification] of classifications) { - this.rows.set( - rowId, - classification.status === 'unknown' - ? { status: 'unknown' } - : { - status: 'exact', - entries: classification.entries.map(({ columnId: _column, ...entry }) => entry), - } - ) + this.rows.set(rowId, classification) } this.accumulator.record(envelopeFromClassifications(classifications, rows.length, this.scope)) } - exportProvenance(): ResolvedSecretTraceProvenanceV1 { - return this.accumulator.exportProvenance() + exportProvenance(deliveredRowIds?: readonly string[]): ResolvedSecretTraceProvenanceV1 { + if (!deliveredRowIds) return this.accumulator.exportProvenance() + const delivered = new Map() + for (const rowId of deliveredRowIds) { + delivered.set(rowId, this.rows.get(rowId) ?? { status: 'unknown', cause: 'uncaptured-row' }) + } + return envelopeFromClassifications(delivered, deliveredRowIds.length, this.scope) } /** One captured row's classification; a row this reader never captured is unknown. */ rowProvenance(rowId: string): DurableSecretProvenance { - return this.rows.get(rowId) ?? { status: 'unknown' } + const classification = this.rows.get(rowId) + return !classification || classification.status === 'unknown' + ? { status: 'unknown' } + : { + status: 'exact', + entries: classification.entries.map(({ columnId: _column, ...entry }) => entry), + } } } diff --git a/apps/sim/lib/table/rows/service.ts b/apps/sim/lib/table/rows/service.ts index b0ce412afdc..1c0c18a1ff1 100644 --- a/apps/sim/lib/table/rows/service.ts +++ b/apps/sim/lib/table/rows/service.ts @@ -881,7 +881,7 @@ export async function upsertRow( mutate: async () => { const [row] = await trx .update(userTableRows) - .set({ data: data.data, updatedAt: now }) + .set(rowContentUpdate(sql`${JSON.stringify(data.data)}::jsonb`, now)) .where( and( eq(userTableRows.id, matchedRowId), @@ -1903,6 +1903,14 @@ function jsonbMergePatch(changedColumnIds: string[], coercedRow: RowData): SQL { return sql`${userTableRows.data} || ${JSON.stringify(patch)}::jsonb` } +function rowContentUpdate(data: SQL, now: Date) { + return { + data, + updatedAt: sql`CASE WHEN ${userTableRows.data} IS DISTINCT FROM ${data} + THEN ${now.toISOString()}::timestamp ELSE ${userTableRows.updatedAt} END`, + } +} + export async function updateRow( data: UpdateRowData, table: TableDefinition, @@ -2064,7 +2072,7 @@ export async function updateRow( patchedColumnIds.size > 0 ? await trx .update(userTableRows) - .set({ data: persistedData, updatedAt: now }) + .set(rowContentUpdate(persistedData, now)) .where(condition) .returning() : await trx.select().from(userTableRows).where(condition).for('update') @@ -2403,10 +2411,7 @@ async function persistBulkUpdateBatch(params: { const batchIds = validIds.slice(index, index + TABLE_LIMITS.UPDATE_BATCH_SIZE) const updated = await trx .update(userTableRows) - .set({ - data: sql`${userTableRows.data} || ${patchJson}::jsonb`, - updatedAt: now, - }) + .set(rowContentUpdate(sql`${userTableRows.data} || ${patchJson}::jsonb`, now)) .where( and( eq(userTableRows.tableId, table.id), @@ -2891,7 +2896,7 @@ export async function batchUpdateRows( const dataPromises = batch.map(({ rowId, changedColumnIds, mergedData }) => trx .update(userTableRows) - .set({ data: jsonbMergePatch(changedColumnIds, mergedData), updatedAt: now }) + .set(rowContentUpdate(jsonbMergePatch(changedColumnIds, mergedData), now)) .where( and( eq(userTableRows.id, rowId), diff --git a/apps/sim/lib/table/rows/writes.integration.ts b/apps/sim/lib/table/rows/writes.integration.ts index 985fb2e4d72..f9bcdb9b9ed 100644 --- a/apps/sim/lib/table/rows/writes.integration.ts +++ b/apps/sim/lib/table/rows/writes.integration.ts @@ -4,7 +4,7 @@ * everywhere; the `rows_version` cases also need the migrated deferred trigger and skip without it. */ import { db } from '@sim/db' -import { userTableDefinitions, userTableRows } from '@sim/db/schema' +import { userTableDefinitions, userTableRowSecretProvenance, userTableRows } from '@sim/db/schema' import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' import { storageServiceMock, storageServiceMockFns } from '@sim/testing/mocks/storage-service.mock' import { tableBillingMock, tableBillingMockFns } from '@sim/testing/mocks/table-billing.mock' @@ -12,7 +12,7 @@ import { tableTriggerMock, tableTriggerMockFns } from '@sim/testing/mocks/table- import { tableWorkflowColumnsMock } from '@sim/testing/mocks/table-workflow-columns.mock' import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' -import { sql } from 'drizzle-orm' +import { eq, sql } from 'drizzle-orm' import postgres from 'postgres' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -21,6 +21,7 @@ vi.mock('@/lib/table/trigger', () => tableTriggerMock) vi.mock('@/lib/table/workflow-columns', () => tableWorkflowColumnsMock) vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock) +import { encryptSecret } from '@/lib/core/security/encryption' import { deleteColumn, updateColumnConstraints } from '@/lib/table/columns/service' import { getMaxRowSizeBytes } from '@/lib/table/constants' import { bulkInsertImportBatch, importAppendRows, importReplaceRows } from '@/lib/table/import-data' @@ -29,6 +30,7 @@ import type { DbTransaction } from '@/lib/table/planner' import { readCurrentRowsVersion } from '@/lib/table/row-changes' import { decodeCursor } from '@/lib/table/rows/cursor' import { lockLiveTableSchema } from '@/lib/table/rows/live-schema' +import { loadTableRowSecretProvenance } from '@/lib/table/rows/secret-provenance' import { batchInsertRows, batchUpdateRows, @@ -45,6 +47,8 @@ import { getTableById } from '@/lib/table/service' import { getOrCreateTableSnapshot } from '@/lib/table/snapshot-cache' import type { ColumnDefinition, JsonValue, RowData, TableDefinition } from '@/lib/table/types' import { checkBatchUniqueConstraintsDb } from '@/lib/table/validation' +import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' +import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' const url = readTestDatabaseUrl() if (process.env.DATABASE_URL !== url) { @@ -90,7 +94,9 @@ async function seedRows( tableId: string, rows: Array<{ id: string; data: RowData; orderKey: string | null; position?: number }> ) { - await db.insert(userTableRows).values(rows.map((row) => ({ ...row, tableId, workspaceId }))) + await db + .insert(userTableRows) + .values(rows.map((row) => ({ ...row, tableId, workspaceId, createdAt: new Date(0) }))) } async function rowsVersion(tableId: string): Promise { @@ -229,6 +235,105 @@ describe('table row writes against real PostgreSQL', () => { await control.end() }) + it.each(['upsert', 'single', 'filter', 'batch'] as const)( + 'preserves the content binding of an identical %s write without healing existing faults', + async (kind) => { + const secret = 'fixture-no-op-content-secret' + const encryptedValue = (await encryptSecret(secret)).encrypted + for (const state of ['exact', 'unknown', 'stale'] as const) { + const table = await createTable([ + { id: 'key', name: 'key', type: 'string', unique: true }, + ...textColumns('note'), + ]) + const rowId = generateId() + const data = { key: 'stable', note: secret } + const revision = new Date('2026-10-01T00:00:00.000Z') + await db.insert(userTableRows).values({ + id: rowId, + tableId: table.id, + workspaceId, + data, + orderKey: 'a0', + createdAt: revision, + updatedAt: revision, + secretProvenanceVersion: 1, + }) + await db.insert(userTableRowSecretProvenance).values({ + rowId, + contentUpdatedAt: state === 'stale' ? new Date(0) : revision, + status: state === 'unknown' ? 'unknown' : 'exact', + entries: + state === 'unknown' + ? [] + : [ + { + columnId: 'note', + encryptedValue, + name: 'ROW_TOKEN', + sourceUserId: userId, + sourceWorkspaceId: workspaceId, + }, + ], + }) + const base = { + tableId: table.id, + workspaceId, + capabilityGovernedUserId: null, + secretProvenance: undefined, + } + const write = async (note: string) => { + if (kind === 'upsert') + await upsertRow( + { ...base, data: { ...data, note }, conflictTarget: 'key' }, + table, + 'noop-provenance' + ) + else if (kind === 'single') + await updateRow({ ...base, rowId, data: { note } }, table, 'noop-provenance') + else if (kind === 'filter') { + const result = await updateRowsByFilter( + table, + { + filter: { key: 'stable' }, + data: { note }, + secretProvenance: undefined, + capabilityGovernedUserId: null, + }, + 'noop-provenance' + ) + expect(result.affectedRowIds).toEqual([rowId]) + } else + await batchUpdateRows( + { ...base, updates: [{ rowId, data: { note } }] }, + table, + 'noop-provenance' + ) + } + await write(secret) + const [stored] = await db.select().from(userTableRows).where(eq(userTableRows.id, rowId)) + expect(stored.updatedAt).toEqual(revision) + const scope = { userId, workspaceId } + const evidence = await loadTableRowSecretProvenance([stored], scope) + expect(evidence.complete).toBe(state === 'exact') + if (state === 'exact') { + const registry = new ResolvedSecretTraceRegistry([], scope) + expect( + await registry.importProvenance(evidence, { trusted: true, origin: 'no-op-read' }) + ).toBe(true) + const shown = projectResolvedSecretModelContent(stored.data, registry) + expect(shown).toMatchObject({ safe: true, value: { note: '{{ROW_TOKEN}}' } }) + } + if (migrated) { + await write('changed content') + const [changed] = await db.select().from(userTableRows).where(eq(userTableRows.id, rowId)) + expect(changed.data).toEqual({ key: 'stable', note: 'changed content' }) + expect(changed.updatedAt.getTime()).toBeGreaterThan(revision.getTime()) + expect(changed.secretProvenanceVersion).toBeNull() + } + } + } + ) + describe('unique columns under concurrent writes', () => { const uniqueColumns: ColumnDefinition[] = [ { id: 'email', name: 'email', type: 'string', unique: true }, diff --git a/apps/sim/lib/tools/falai-pricing.test.ts b/apps/sim/lib/tools/falai-pricing.test.ts index 1cb90c7fd8d..65d8c3187df 100644 --- a/apps/sim/lib/tools/falai-pricing.test.ts +++ b/apps/sim/lib/tools/falai-pricing.test.ts @@ -47,6 +47,26 @@ describe('getFalAICostMetadata fallback floor', () => { expect(result.costDollars).toBe(expected) }) + it('bills the floor when the pricing estimate answers with an unreadable body', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async (url: string | URL) => + String(url).includes('pricing/estimate') + ? new Response('not json', { status: 200 }) + : new Response('error', { status: 500 }) + ) + ) + + const result = await getFalAICostMetadata({ + apiKey: 'test-key', + endpointId: 'fal-ai/veo-3.1', + requestId: 'req_123', + }) + + expect(result.source).toBe('fallback_floor') + expect(result.costDollars).toBe(FALAI_VIDEO_FALLBACK_PROVIDER_COST_DOLLARS) + }) + it('never bills an audio clip at the video floor', () => { expect(FALAI_AUDIO_FALLBACK_PROVIDER_COST_DOLLARS).toBeLessThan( FALAI_VIDEO_FALLBACK_PROVIDER_COST_DOLLARS diff --git a/apps/sim/lib/tools/falai-pricing.ts b/apps/sim/lib/tools/falai-pricing.ts index 0695a7bf0d1..b31a1cbf428 100644 --- a/apps/sim/lib/tools/falai-pricing.ts +++ b/apps/sim/lib/tools/falai-pricing.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { interruptibleSleep } from '@sim/utils/helpers' import { isRecordLike } from '@sim/utils/object' +import { DeadlineExceededError, withinDeadline } from '@/lib/core/utils/deadline' import { DEFAULT_MAX_ERROR_BODY_BYTES, readResponseJsonWithLimit, @@ -15,6 +16,11 @@ export const FALAI_VIDEO_FALLBACK_PROVIDER_COST_DOLLARS = 0.25 const FALAI_BILLING_EVENT_ATTEMPTS = 2 const FALAI_BILLING_EVENT_RETRY_MS = 500 const FALAI_PRICING_RESPONSE_MAX_BYTES = 2 * 1024 * 1024 +/** + * Longest a cost lookup may take, covering every billing-event attempt and the estimate. The + * generation is already paid for by then, so a lookup that runs out falls back to the floor. + */ +const FALAI_COST_LOOKUP_TIMEOUT_MS = 10_000 const logger = createLogger('FalAIPricing') export interface FalAICostMetadata { @@ -167,10 +173,16 @@ async function estimateFalAICallCost( return { error: `Fal.ai pricing estimate failed: ${response.status} ${error}` } } - const data = await readResponseJsonWithLimit(response, { - maxBytes: FALAI_PRICING_RESPONSE_MAX_BYTES, - label: 'Fal.ai pricing estimate response', - }) + let data: unknown + try { + data = await readResponseJsonWithLimit(response, { + maxBytes: FALAI_PRICING_RESPONSE_MAX_BYTES, + label: 'Fal.ai pricing estimate response', + }) + } catch (error) { + signal?.throwIfAborted() + return { error: getErrorMessage(error, 'Unknown error') } + } const totalCost = isRecordLike(data) ? getNumber(data.total_cost) : undefined if (totalCost === undefined) { return { error: 'Fal.ai pricing estimate missing total_cost' } @@ -179,18 +191,57 @@ async function estimateFalAICallCost( return { costDollars: totalCost } } -export async function getFalAICostMetadata({ - apiKey, - endpointId, - requestId, - signal, -}: { +interface FalAICostLookup { apiKey: string endpointId: string requestId: string + /** Ends the lookup with an error. Without one, only {@link FALAI_COST_LOOKUP_TIMEOUT_MS} ends it. */ signal?: AbortSignal -}): Promise { - signal?.throwIfAborted() +} + +/** + * The cost of a completed fal request. The lookup is bounded on its own: past + * {@link FALAI_COST_LOOKUP_TIMEOUT_MS} it returns the fallback floor rather than nothing. + */ +export async function getFalAICostMetadata(lookup: FalAICostLookup): Promise { + const { endpointId, requestId } = lookup + try { + return await withinDeadline( + (signal) => lookUpFalAICost({ ...lookup, signal }), + Date.now() + FALAI_COST_LOOKUP_TIMEOUT_MS, + lookup.signal + ) + } catch (error) { + if (!(error instanceof DeadlineExceededError)) throw error + logger.warn('Fal.ai cost lookup timed out after generation completed', { + endpointId, + requestId, + }) + return fallbackFloorCost(endpointId, requestId, error.message) + } +} + +function fallbackFloorCost( + endpointId: string, + requestId: string, + error: string | undefined +): FalAICostMetadata { + return { + endpointId, + requestId, + costDollars: getFalAIFallbackProviderCostDollars(endpointId), + source: 'fallback_floor', + currency: 'USD', + error, + } +} + +async function lookUpFalAICost({ + apiKey, + endpointId, + requestId, + signal, +}: FalAICostLookup): Promise { for (let attempt = 0; attempt < FALAI_BILLING_EVENT_ATTEMPTS; attempt++) { const event = await fetchFalAIBillingEvent(apiKey, requestId, signal) if (event) { @@ -229,12 +280,5 @@ export async function getFalAICostMetadata({ error: estimate.error, }) - return { - endpointId, - requestId, - costDollars: getFalAIFallbackProviderCostDollars(endpointId), - source: 'fallback_floor', - currency: 'USD', - error: estimate.error, - } + return fallbackFloorCost(endpointId, requestId, estimate.error) } diff --git a/apps/sim/lib/uploads/contexts/workspace/file-secret-provenance.test.ts b/apps/sim/lib/uploads/contexts/workspace/file-secret-provenance.test.ts index a45b0a0191c..4d92ebe9fa5 100644 --- a/apps/sim/lib/uploads/contexts/workspace/file-secret-provenance.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/file-secret-provenance.test.ts @@ -889,13 +889,13 @@ describe('workspace file secret provenance', () => { ) }) - it('does not remint named provenance into a copied file with a different owner scope', async () => { + it('rejects malformed copied evidence without a source identity', async () => { const targetContentUpdatedAt = new Date('2026-08-04T00:00:02.000Z') dbChainMockFns.limit .mockResolvedValueOnce([ { - userId: 'target-user', - workspaceId: 'target-workspace', + userId: 'source-user', + workspaceId: 'source-workspace', contentUpdatedAt: targetContentUpdatedAt, }, ]) diff --git a/apps/sim/lib/uploads/contexts/workspace/file-secret-provenance.ts b/apps/sim/lib/uploads/contexts/workspace/file-secret-provenance.ts index d31282b58da..eabd9064035 100644 --- a/apps/sim/lib/uploads/contexts/workspace/file-secret-provenance.ts +++ b/apps/sim/lib/uploads/contexts/workspace/file-secret-provenance.ts @@ -4,6 +4,7 @@ import { type WorkspaceFileSecretProvenanceEntry, workspaceFileSecretProvenance, workspaceFiles, + workspaceFileVersion, } from '@sim/db/schema' import { compareStrings } from '@sim/utils/string' import { and, desc, eq, gte, inArray, isNull, lt, or, sql } from 'drizzle-orm' @@ -710,7 +711,7 @@ export async function applyWorkspaceFileSecretProvenancePolicyInTx( return snapshotWorkspaceFileSecretProvenanceInTx(tx, fileId, nextContentUpdatedAt, 1) } -/** Copies exact provenance for a byte-identical, same-owner-scope file copy; otherwise unknown. */ +/** Preserves encrypted origin entries for an authorized copy of the captured file revision. */ export async function copyWorkspaceFileSecretProvenanceInTx( tx: DbTransaction, expectedSource: WorkspaceFileSecretProvenanceCopySource | undefined, @@ -718,8 +719,6 @@ export async function copyWorkspaceFileSecretProvenanceInTx( ): Promise { const [target] = await tx .select({ - userId: workspaceFiles.userId, - workspaceId: workspaceFiles.workspaceId, contentUpdatedAt: workspaceFiles.contentUpdatedAt, }) .from(workspaceFiles) @@ -739,8 +738,6 @@ export async function copyWorkspaceFileSecretProvenanceInTx( const [source] = await tx .select({ key: workspaceFiles.key, - userId: workspaceFiles.userId, - workspaceId: workspaceFiles.workspaceId, secretProvenanceVersion: workspaceFiles.secretProvenanceVersion, fileContentUpdatedAt: workspaceFiles.contentUpdatedAt, provenanceContentUpdatedAt: workspaceFileSecretProvenance.contentUpdatedAt, @@ -780,28 +777,13 @@ export async function copyWorkspaceFileSecretProvenanceInTx( }) return } - /** - * An absence copies as an absence. Folding it in with the refusals below would hand the target a - * taint the source never carried, and a fork or a chat copy would turn a readable file into a - * permanently refused one — the pathology this surface exists to undo, reached by copying. - * - * Safe to carry across the scope checks below, which exist to stop one workspace's secret entries - * landing in another's file. A recorded absence has no entries to carry. - */ if (source.status === 'unrecorded') { await replaceWorkspaceFileSecretProvenanceInTx(tx, targetFileId, target.contentUpdatedAt, { status: 'unrecorded', }) return } - if ( - source.status !== 'exact' || - (source.entries.length > 0 && - (!source.workspaceId || - !target.workspaceId || - source.userId !== target.userId || - source.workspaceId !== target.workspaceId)) - ) { + if (source.status !== 'exact') { await replaceWorkspaceFileSecretProvenanceInTx(tx, targetFileId, target.contentUpdatedAt, { status: 'unknown', }) @@ -818,7 +800,7 @@ export async function markWorkspaceFileSecretProvenanceUnknown( workspaceId: string, fileIds: readonly string[] ): Promise { - const uniqueIds = [...new Set(fileIds.filter((fileId) => fileId.length > 0))] + const uniqueIds = [...new Set(fileIds.filter((fileId) => fileId.length > 0))].sort(compareStrings) if (uniqueIds.length === 0) return await db.transaction(async (tx) => { @@ -833,7 +815,11 @@ export async function markWorkspaceFileSecretProvenanceUnknown( for (let index = 0; index < uniqueIds.length; index += FILE_PROVENANCE_QUERY_CHUNK_SIZE) { const chunk = uniqueIds.slice(index, index + FILE_PROVENANCE_QUERY_CHUNK_SIZE) const rows = await tx - .select({ id: workspaceFiles.id, contentUpdatedAt: workspaceFiles.contentUpdatedAt }) + .select({ + id: workspaceFiles.id, + key: workspaceFiles.key, + contentUpdatedAt: workspaceFiles.contentUpdatedAt, + }) .from(workspaceFiles) .where( and( @@ -843,11 +829,26 @@ export async function markWorkspaceFileSecretProvenanceUnknown( isNull(workspaceFiles.deletedAt) ) ) + .orderBy(workspaceFiles.id) + .for('update') bound += rows.length for (const row of rows) { await replaceWorkspaceFileSecretProvenanceInTx(tx, row.id, row.contentUpdatedAt, { status: 'unknown', }) + await tx + .update(workspaceFileVersion) + .set({ + secretProvenanceStatus: 'unknown', + secretProvenanceEntries: [], + }) + .where( + and( + eq(workspaceFileVersion.fileId, row.id), + eq(workspaceFileVersion.key, row.key), + isNull(workspaceFileVersion.supersededAt) + ) + ) } } /** @@ -1443,12 +1444,7 @@ async function loadModelSafeWorkspaceFileRowChunk( workspaceFileSecretProvenance, eq(workspaceFileSecretProvenance.fileId, workspaceFiles.id) ) - .where( - and( - inArray(workspaceFiles.key, [...keys]), - or(isNull(workspaceFiles.deletedAt), eq(workspaceFiles.context, 'execution')) - ) - ) + .where(inArray(workspaceFiles.key, [...keys])) .orderBy( workspaceFiles.key, sql`${workspaceFiles.deletedAt} IS NULL DESC`, diff --git a/apps/sim/lib/users/account-deletion-run-stops.test.ts b/apps/sim/lib/users/account-deletion-run-stops.test.ts new file mode 100644 index 00000000000..ff107260edb --- /dev/null +++ b/apps/sim/lib/users/account-deletion-run-stops.test.ts @@ -0,0 +1,51 @@ +import { dbChainMockFns, resetDbChainMock } from '@sim/testing' +import { billingPlanMock, billingPlanMockFns } from '@sim/testing/mocks/billing-plan.mock' +import { + mothershipAgentUrlMock, + mothershipAgentUrlMockFns, +} from '@sim/testing/mocks/mothership-agent-url.mock' +import { + organizationMembershipMock, + organizationMembershipMockFns, +} from '@sim/testing/mocks/organization-membership.mock' +import { storageServiceMockFns } from '@sim/testing/mocks/storage-service.mock' +import { tableEventsMock, tableEventsMockFns } from '@sim/testing/mocks/table-events.mock' +import { uploadsMock, uploadsMockFns } from '@sim/testing/mocks/uploads.mock' +import { workspacesUtilsMock } from '@sim/testing/mocks/workspaces-utils.mock' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/billing/organizations/membership', () => organizationMembershipMock) +vi.mock('@/lib/billing/core/plan', () => billingPlanMock) +vi.mock('@/lib/uploads', () => uploadsMock) +vi.mock('@/lib/workspaces/utils', () => workspacesUtilsMock) +vi.mock('@/lib/table/events', () => tableEventsMock) +vi.mock('@/lib/mothership/server/agent-url', () => mothershipAgentUrlMock) + +import { deleteUserAccount } from '@/lib/users/account-deletion' + +describe('stopping a deleted account’s Chat runs', () => { + beforeEach(() => { + resetDbChainMock() + organizationMembershipMockFns.mockIsSoleOwnerOfPaidOrganization.mockResolvedValue({ + isSoleOwner: false, + name: null, + }) + billingPlanMockFns.mockGetHighestPriorityPersonalSubscription.mockResolvedValue(null) + uploadsMockFns.mockIsUsingCloudStorage.mockReturnValue(false) + storageServiceMockFns.mockDeleteFiles.mockResolvedValue({ failed: [] }) + tableEventsMockFns.mockAppendTableEvent.mockResolvedValue(null) + }) + + /** + * The worker endpoint only routes the best-effort Stop push; run control answers Stop for + * the erased account either way, so the erasure must not depend on reading it. + */ + it('still erases the account when the run-routing lookup fails', async () => { + mothershipAgentUrlMockFns.mockGetMothershipBaseURL.mockRejectedValueOnce( + new Error('settings unavailable') + ) + + await expect(deleteUserAccount('user-1')).resolves.toBeDefined() + expect(dbChainMockFns.transaction).toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/users/account-deletion.ts b/apps/sim/lib/users/account-deletion.ts index 4889fd67fc6..ca0c883f95f 100644 --- a/apps/sim/lib/users/account-deletion.ts +++ b/apps/sim/lib/users/account-deletion.ts @@ -18,7 +18,7 @@ import { workspace as workspaceTable, } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' +import { getErrorMessage, getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' import { formatQuotedNameList } from '@sim/utils/string' import { and, eq, gt, inArray, isNotNull, isNull, lte, ne, notExists, or, sql } from 'drizzle-orm' import type { @@ -39,6 +39,10 @@ import { } from '@/lib/billing/organizations/resource-handoff' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { DbOrTx } from '@/lib/db/types' +import { type StoppedRun, stopLiveRunsTx } from '@/lib/mothership/async-runs/repository' +import { enqueueMemoryCleanup } from '@/lib/mothership/memories/cleanup-outbox' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' +import { getMothershipBaseURL } from '@/lib/mothership/server/agent-url' import { getProjectAccountDeletionBlockers, prepareProjectsForAccountDeletion, @@ -777,6 +781,18 @@ export async function deleteUserAccount(userId: string): Promise { + logger.warn('Run routing for the deleted account could not be read', { + userId, + error: getErrorMessage(error), + }) + return undefined + }) await db.transaction(async (tx) => { /** Organization and Project gates precede workspace rows, which precede user FK barriers. */ @@ -840,6 +856,11 @@ export async function deleteUserAccount(userId: string): Promise 0) { /** * Re-checked here rather than trusted from the plan: a workspace that @@ -965,6 +986,12 @@ export async function deleteUserAccount(userId: string): Promise ({ kind: 'workspace' as const, workspaceId })), + ...projectIdsToDelete.map((projectId) => ({ kind: 'project' as const, projectId })), + ]) + try { await tx.delete(user).where(eq(user.id, userId)) } catch (error) { @@ -990,6 +1017,8 @@ export async function deleteUserAccount(userId: string): Promise { }) /** Version 2 holds a named secret; version 3 replaced it with public text. */ - async function seedVersionedFile(historicalProvenance?: WorkspaceFileSecretProvenance) { + async function seedVersionedFile( + historicalProvenance?: WorkspaceFileSecretProvenance, + markHistoricalUnknown = false + ) { const ids = createKnowledgeAclFixtureIds() fixtures.push(ids) await seedKnowledgeAclFixture(ids) @@ -124,6 +130,9 @@ describe('chat-delegated file version history', () => { secretProvenancePolicy: { mode: 'replace', provenance }, } ) + if (markHistoricalUnknown && content.startsWith('token=')) { + await markWorkspaceFileSecretProvenanceUnknown(ids.workspaceId, [file.id]) + } } return { ...ids, fileId: file.id } } @@ -397,6 +406,31 @@ describe('chat-delegated file version history', () => { expect(registry.isPermanentlyIncomplete()).toBe(false) }) + it('retains a classification-only refusal when the current version becomes historical and is reverted', async () => { + const fixture = await seedVersionedFile(undefined, true) + const chat = chatTransport(fixture, fixture.aliceId, registryFor(fixture, fixture.aliceId)) + const older = await chat( + `/api/v2/files/${fixture.fileId}/versions/1/text?workspaceId=${fixture.workspaceId}` + ) + expect(older.status).toBe(200) + const historical = await chat( + `/api/v2/files/${fixture.fileId}/versions/2/text?workspaceId=${fixture.workspaceId}` + ) + expect(historical.status).toBe(403) + expect(await historical.text()).not.toContain(SECRET) + const reverted = await chat(`/api/v2/files/${fixture.fileId}/versions/2/revert`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ workspaceId: fixture.workspaceId }), + }) + expect(reverted.status).toBe(200) + const current = await chat( + `/api/v2/files/${fixture.fileId}/text?workspaceId=${fixture.workspaceId}` + ) + expect(current.status).toBe(403) + expect(await current.text()).not.toContain(SECRET) + }) + it('keeps a secret redacted after Chat reverts the file to the version holding it', async () => { const fixture = await seedVersionedFile() const reverted = await chatTransport(fixture, fixture.aliceId)( diff --git a/apps/sim/lib/workspace-files/application/subject.ts b/apps/sim/lib/workspace-files/application/subject.ts index a11bc5906e7..81145e11930 100644 --- a/apps/sim/lib/workspace-files/application/subject.ts +++ b/apps/sim/lib/workspace-files/application/subject.ts @@ -17,19 +17,23 @@ export async function requireFileSubject(principal: FileSubjectPrincipal): Promi return userId } -/** Holds the acting account stable through a file transaction without substituting an owner. */ +/** + * Holds the acting account stable through a file transaction without substituting an owner. + * `snapshot` reads it without a lock, for a caller in a read-only snapshot that acts on nothing. + */ export async function requireCurrentFileSubject( tx: DbTransaction, - principal: FileSubjectPrincipal + principal: FileSubjectPrincipal, + mode: 'hold' | 'snapshot' = 'hold' ): Promise { // actorless-unsupported: Project files and cross-owner copies require a human; their operation registries reject workspace keys and executors. const userId = requirePrincipalSubjectUserId(principal) - const [actor] = await tx + const query = tx .select({ banned: user.banned, banExpires: user.banExpires, suspendedAt: user.suspendedAt }) .from(user) .where(eq(user.id, userId)) - .for('share') .limit(1) + const [actor] = await (mode === 'hold' ? query.for('share') : query) if (!actor || isAccountBlocked(actor)) { throw new OrchestrationError('forbidden', 'User account is suspended') } diff --git a/apps/sim/lib/workspace-files/archive-extraction.ts b/apps/sim/lib/workspace-files/archive-extraction.ts index 54a389d6de2..2530f27821d 100644 --- a/apps/sim/lib/workspace-files/archive-extraction.ts +++ b/apps/sim/lib/workspace-files/archive-extraction.ts @@ -129,11 +129,6 @@ export async function commitFileArchiveInTx( tx ) const folders = new Map([['[]', root.id]]) - const provenance: WorkspaceFileSecretProvenance = - args.secretProvenance.status === 'unrecorded' || - (args.secretProvenance.status === 'exact' && args.secretProvenance.entries.length === 0) - ? args.secretProvenance - : { status: 'unknown' } for (const entry of args.staged.entries) { args.signal.throwIfAborted() let parentId = root.id @@ -161,7 +156,7 @@ export async function commitFileArchiveInTx( staged: entry.content, folderId: parentId, exactName: false, - secretProvenance: provenance, + secretProvenance: args.secretProvenance, }) } args.signal.throwIfAborted() diff --git a/apps/sim/lib/workspace-files/search/artifact-ready.ts b/apps/sim/lib/workspace-files/search/artifact-ready.ts index 96e1b83fae9..c41698dc70f 100644 --- a/apps/sim/lib/workspace-files/search/artifact-ready.ts +++ b/apps/sim/lib/workspace-files/search/artifact-ready.ts @@ -4,7 +4,7 @@ import { workspaceFileSearchBuild, workspaceFileSearchRevision, } from '@sim/db/schema' -import { and, eq, or, sql } from 'drizzle-orm' +import { and, eq, inArray, or, sql } from 'drizzle-orm' import type { DbTransaction } from '@/lib/db/types' import type { EditableFileOwner } from '@/lib/workspace-files/ownership' import { FILE_SEARCH_ARTIFACT_READY_LOCK_TIMEOUT_MS } from '@/lib/workspace-files/search/constants' @@ -51,7 +51,8 @@ function artifactRevisionCondition(input: FileSearchArtifactCompletion, owner: E /** * Lock-free form of {@link markFileSearchArtifactReadyInTx}'s no-op test: the file's current - * revision is already built from this artifact, ready or still under a live build lease. + * revision is already built from this artifact, published (ready or skipped) or still under a live + * build lease. */ async function isFileSearchArtifactCurrent( input: FileSearchArtifactCompletion, @@ -69,7 +70,7 @@ async function isFileSearchArtifactCurrent( artifactRevisionCondition(input, owner), eq(workspaceFileSearchBuild.artifactKey, input.artifactKey), or( - eq(workspaceFileSearchRevision.status, 'ready'), + inArray(workspaceFileSearchRevision.status, ['ready', 'skipped']), and( eq(workspaceFileSearchRevision.status, 'pending'), sql`${workspaceFileSearchBuild.expiresAt} > clock_timestamp()` @@ -128,7 +129,9 @@ export async function markFileSearchArtifactReadyInTx( .for('update') if ( build?.artifactKey === input.artifactKey && - (state.status === 'ready' || (state.status === 'pending' && build.live)) + (state.status === 'ready' || + state.status === 'skipped' || + (state.status === 'pending' && build.live)) ) return false await tx diff --git a/apps/sim/lib/workspace-files/search/index-state.ts b/apps/sim/lib/workspace-files/search/index-state.ts index f555acbd6a9..014c670f475 100644 --- a/apps/sim/lib/workspace-files/search/index-state.ts +++ b/apps/sim/lib/workspace-files/search/index-state.ts @@ -298,18 +298,26 @@ export async function publishFileSearchBuild( publication.dependencies.map((dependency) => ({ ...dependency, buildId: build.id })) ) } + /** + * A skipped build stays published, with its bound artifact key, so a rendered read of the same + * artifact sees it as current instead of requeueing it. It keeps no text. + */ + if (publication.status === 'skipped') + await tx + .delete(workspaceFileSearchChunk) + .where(eq(workspaceFileSearchChunk.buildId, build.id)) await tx .update(workspaceFileSearchBuild) .set({ - expiresAt: publication.status === 'ready' ? null : sql`clock_timestamp()`, - artifactKey: publication.status === 'ready' ? (publication.artifactKey ?? null) : null, + expiresAt: null, + ...(publication.status === 'ready' ? { artifactKey: publication.artifactKey ?? null } : {}), }) .where(eq(workspaceFileSearchBuild.id, build.id)) await tx .update(workspaceFileSearchRevision) .set({ status: publication.status, - buildId: publication.status === 'ready' ? build.id : null, + buildId: build.id, failureReason: publication.status === 'skipped' ? publication.failureReason : null, lineCount: publication.status === 'ready' ? publication.lineCount : 0, indexedBytes: publication.status === 'ready' ? publication.indexedBytes : 0, diff --git a/apps/sim/lib/workspace-files/search/owners.integration.ts b/apps/sim/lib/workspace-files/search/owners.integration.ts index ceb5422dca3..3eede26617d 100644 --- a/apps/sim/lib/workspace-files/search/owners.integration.ts +++ b/apps/sim/lib/workspace-files/search/owners.integration.ts @@ -66,7 +66,10 @@ import { beginFileSearchBuild, publishFileSearchBuild, } from '@/lib/workspace-files/search/index-state' -import { indexWorkspaceFileForSearch } from '@/lib/workspace-files/search/indexing' +import { + indexWorkspaceFileForSearch, + markWorkspaceFileSearchIndexFailed, +} from '@/lib/workspace-files/search/indexing' async function requireDocxFormat() { const format = await getE2BDocFormat('report.docx') @@ -911,6 +914,45 @@ describe('owner-scoped indexed file search on PostgreSQL and stored bytes', () = expect(unchanged.buildId).toBe(ready.buildId) } ) + check('keeps a skipped artifact build on later reads but requeues a failed one', async () => { + const f = await fixture() + const source = 'binaryartifactsource' + const generated = await create(f, source, { name: 'binary.docx' }) + await storeCompiledDoc( + f.owner, + source, + 'docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + Buffer.concat([Buffer.from('PK\x03\x04'), Buffer.alloc(64), Buffer.from([0xff, 0xfe])]), + fileDocumentCacheIdentity(source, await requireDocxFormat(), 'text/plain') + ) + const input = { projectId: f.projectId, fileId: generated.file.id, maxBytes: 1_000_000 } + const revisionOf = async () => { + const [row] = await db + .select() + .from(workspaceFileSearchRevision) + .where(eq(workspaceFileSearchRevision.fileId, generated.file.id)) + return row + } + await readProjectFileArtifact.execute({ principal: f.reader, input }) + await markWorkspaceFileSearchIndexFailed({ + owner: f.owner, + ...(await claim(generated.file.id)), + }) + expect((await revisionOf()).status).toBe('failed') + await readProjectFileArtifact.execute({ principal: f.reader, input }) + expect((await revisionOf()).status).toBe('pending') + await indexWorkspaceFileForSearch( + { owner: f.owner, ...(await claim(generated.file.id)) }, + signal + ) + const skipped = await revisionOf() + expect(skipped).toMatchObject({ status: 'skipped', failureReason: 'binary_or_degraded' }) + await readProjectFileArtifact.execute({ principal: f.reader, input }) + const unchanged = await revisionOf() + expect(unchanged.status).toBe('skipped') + expect(unchanged.buildId).toBe(skipped.buildId) + }) check('retains workspace generated-input provenance in indexed snippet delivery', async () => { const f = await fixture() diff --git a/apps/sim/lib/workspaces/access/workspace-access.ts b/apps/sim/lib/workspaces/access/workspace-access.ts index 8c4da6b7678..9bae43cf9fb 100644 --- a/apps/sim/lib/workspaces/access/workspace-access.ts +++ b/apps/sim/lib/workspaces/access/workspace-access.ts @@ -1,9 +1,17 @@ import { permissions, workspace } from '@sim/db/schema' -import type { PermissionType } from '@sim/platform-authz/workspace' +import { + type PermissionType, + resolveEffectiveWorkspacePermission, +} from '@sim/platform-authz/workspace' import { generateId } from '@sim/utils/id' -import { and, eq } from 'drizzle-orm' +import { and, eq, inArray } from 'drizzle-orm' import { revokeWorkspaceCredentialMembershipsTx } from '@/lib/credentials/access' import type { DbOrTx, DbTransaction } from '@/lib/db/types' +import { + findWorkspacesWithLiveRunsTx, + type StoppedRun, + stopLiveRunsTx, +} from '@/lib/mothership/async-runs/repository' import { lockProjectsForResourceDepartureTx } from '@/lib/projects/resource-handoff' import { removeWorkspaceSkillMembershipsTx } from '@/lib/skills/access' import { reassignSharedResourceOwnershipForWorkspaceMemberRemovalTx } from '@/lib/workspaces/resource-handoff' @@ -113,8 +121,13 @@ export async function lowerWorkspaceAccessTx( } export type RevokeWorkspaceAccessResult = - /** `ownershipTransferred` is true when the departing user owned the workspace and it moved to the billed account. */ - | { revoked: true; ownershipTransferred: boolean } + /** + * `ownershipTransferred` is true when the departing user owned the workspace and it moved to + * the billed account. `stoppedRuns` are the user's Chat runs there, stopped in this + * transaction when no access remains (see {@link stopRunsWithoutWorkspaceAccessTx}); deliver + * them with `deliverRunStops` after commit. + */ + | { revoked: true; ownershipTransferred: boolean; stoppedRuns: StoppedRun[] } /** Workspace IDs whose shared resources or ownership lack a successor; access remains unchanged. */ | { revoked: false; reason: 'unresolved-workflows'; unresolvedWorkspaceIds: string[] } /** The requested membership no longer exists; a replacement grant must remain untouched. */ @@ -123,7 +136,16 @@ export type RevokeWorkspaceAccessResult = /** Reassigns shared resources and workspace ownership before revoking access in the same transaction. */ export async function revokeWorkspaceAccessTx( tx: DbTransaction, - params: { workspaceId: string; userId: string; expectedPermissionId?: string } + params: { + workspaceId: string + userId: string + expectedPermissionId?: string + /** + * Leaves the user's runs to a caller that changes more access in this transaction and + * stops runs once that access is final; `stoppedRuns` is then empty. + */ + deferRunStops?: boolean + } ): Promise { await lockProjectsForResourceDepartureTx(tx, [params.workspaceId]) if (params.expectedPermissionId !== undefined) { @@ -183,8 +205,35 @@ export async function revokeWorkspaceAccessTx( await revokeWorkspaceCredentialMembershipsTx(tx, params.workspaceId, params.userId) await removeWorkspaceSkillMembershipsTx(tx, params.workspaceId, params.userId) + const stoppedRuns = params.deferRunStops + ? [] + : await stopRunsWithoutWorkspaceAccessTx(tx, params.userId, [params.workspaceId]) + + return { revoked: true, ownershipTransferred, stoppedRuns } +} - return { revoked: true, ownershipTransferred } +/** + * Stops the user's live Chat runs in each of these workspaces where they hold no effective + * access any more. An organization admin keeps derived access without an explicit grant, so + * withdrawing a grant or a role ends runs only where access actually ended. + */ +export async function stopRunsWithoutWorkspaceAccessTx( + tx: DbTransaction, + userId: string, + workspaceIds: string[] +): Promise { + /** Access is checked only where a run could be ended, not across a whole organization. */ + const withRuns = await findWorkspacesWithLiveRunsTx(tx, userId, workspaceIds) + if (withRuns.length === 0) return [] + const owners = await tx + .select({ id: workspace.id, organizationId: workspace.organizationId }) + .from(workspace) + .where(inArray(workspace.id, withRuns)) + const lost: string[] = [] + for (const { id, organizationId } of owners) { + if (!(await resolveEffectiveWorkspacePermission(userId, id, organizationId, tx))) lost.push(id) + } + return stopLiveRunsTx(tx, { userId, workspaceIds: lost }) } /** The permission a user currently holds on a workspace, if any. */ diff --git a/apps/sim/lib/workspaces/application/remove-member.test.ts b/apps/sim/lib/workspaces/application/remove-member.test.ts index 270592d1c5b..2ba5a95a979 100644 --- a/apps/sim/lib/workspaces/application/remove-member.test.ts +++ b/apps/sim/lib/workspaces/application/remove-member.test.ts @@ -101,7 +101,7 @@ describe('workspace member removal', () => { }) mocks.role.mockResolvedValue('admin') mocks.orgAdmin.mockResolvedValue(false) - mocks.revoke.mockResolvedValue({ revoked: true, ownershipTransferred: false }) + mocks.revoke.mockResolvedValue({ revoked: true, ownershipTransferred: false, stoppedRuns: [] }) mocks.removeOrg.mockResolvedValue({ success: true, removed: true }) mocks.seats.mockResolvedValue({ changed: true }) authMockFns.mockGetSession.mockResolvedValue({ @@ -131,7 +131,7 @@ describe('workspace member removal', () => { it('allows owner-only membership to transfer to the billing account', async () => { queueWorkspace({ ownerId: 'target', missingTarget: true }) - mocks.revoke.mockResolvedValue({ revoked: true, ownershipTransferred: true }) + mocks.revoke.mockResolvedValue({ revoked: true, ownershipTransferred: true, stoppedRuns: [] }) expect(await remove()).toMatchObject({ ownershipTransferred: true, removedUserRole: 'owner' }) }) diff --git a/apps/sim/lib/workspaces/lifecycle.ts b/apps/sim/lib/workspaces/lifecycle.ts index 7a85dc15fda..831e57d0d4d 100644 --- a/apps/sim/lib/workspaces/lifecycle.ts +++ b/apps/sim/lib/workspaces/lifecycle.ts @@ -21,6 +21,7 @@ import { mapWithConcurrency } from '@/lib/core/utils/concurrency' import type { DbTransaction } from '@/lib/db/types' import { mcpPubSub } from '@/lib/mcp/pubsub' import { mcpService } from '@/lib/mcp/service' +import { type StoppedRun, stopLiveRunsTx } from '@/lib/mothership/async-runs/repository' import { archiveProjectWithLastEnvironment, lockWorkspaceProjectExclusive, @@ -40,11 +41,22 @@ export interface EnvironmentArchiveEffects { workspaceId: string workflows: { id: string; serverIds: string[] }[] serverIds: string[] + /** + * Chat runs the archive stopped. The caller delivers their Stop with `deliverRunStops`: + * this module sits in page graphs that must not load that delivery. + */ + stoppedRuns: StoppedRun[] } interface ArchiveWorkspaceOptions { requestId: string expectedOwnerId?: string + /** + * Starts delivering the Stop the archive committed for its live Chat runs, before the slower + * post-commit cleanup. Injected because this module sits in page graphs that must not load + * the delivery; without it the runs stop on run control's next read. + */ + deliverStops?: (runs: StoppedRun[]) => void } interface ArchiveWorkspaceResult { @@ -90,6 +102,7 @@ export async function archiveWorkspace( if (!outcome) return { archived: false } logger.info(`[${options.requestId}] Archived workspace ${workspaceId}`) + options.deliverStops?.(outcome.effects.stoppedRuns) await finishEnvironmentArchive(outcome.effects, options.requestId) @@ -101,9 +114,9 @@ export async function archiveWorkspace( } /** - * Archives an environment and its active workflows in the caller's transaction, batching - * the workflow statements; callers own the Project lifecycle. Announce the returned - * effects with {@link finishEnvironmentArchive} after commit. + * Archives an environment, its active workflows and its live Chat runs in the caller's + * transaction, batching the workflow statements; callers own the Project lifecycle. Announce + * the returned effects with {@link finishEnvironmentArchive} after commit. */ export async function archiveEnvironmentInTransaction( tx: DbTransaction, @@ -134,7 +147,8 @@ export async function archiveEnvironmentInTransaction( for (const id of batch) workflows.push({ id, serverIds: serverIdsByWorkflow.get(id) ?? [] }) } const serverIds = await archiveWorkspaceRecordsInTransaction(tx, workspaceId, now) - return { workspaceId, workflows, serverIds } + const stoppedRuns = await stopLiveRunsTx(tx, { workspaceId }) + return { workspaceId, workflows, serverIds, stoppedRuns } } /** diff --git a/apps/sim/lib/workspaces/permissions/member-removal-store.ts b/apps/sim/lib/workspaces/permissions/member-removal-store.ts index bc5b5a7573c..8f37f458230 100644 --- a/apps/sim/lib/workspaces/permissions/member-removal-store.ts +++ b/apps/sim/lib/workspaces/permissions/member-removal-store.ts @@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm' import { removeUserFromOrganization } from '@/lib/billing/organizations/membership' import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats' import { OrchestrationError } from '@/lib/core/orchestration/types' +import { deliverRunStops } from '@/lib/mothership/request/application/stop-delivery' import { revokeWorkspaceAccessTx } from '@/lib/workspaces/access/workspace-access' import { isOrganizationAdminOrOwner } from '@/lib/workspaces/permissions/utils' import { WorkspaceBillingAccountRemovalError } from '@/lib/workspaces/utils' @@ -113,6 +114,7 @@ export async function removeWorkspaceMemberRecord( return result }) const { ownershipTransferred } = revocation + deliverRunStops(revocation.stoppedRuns) /** * Seats are tied to organization membership (one per member), so a @@ -150,6 +152,9 @@ export async function removeWorkspaceMemberRecord( /** Leaving a workspace must not sign the leaver out of Sim. */ ...(spareSessionId ? { spareSessionId } : {}), }) + /** The workspace's runs were delivered with the revocation above. */ + const delivered = new Set(revocation.stoppedRuns.map((run) => run.streamId)) + deliverRunStops(removal.stoppedRuns.filter((run) => !delivered.has(run.streamId))) if (removal.success && removal.removed) { organizationRemoval = true diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index 2f07796494b..e8914a48367 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -668,16 +668,40 @@ const nextConfig: NextConfig = { return redirects }, async rewrites() { - return [ - { - source: '/favicon.ico', - destination: '/icon.svg', - }, - { - source: '/r/:shortCode', - destination: 'https://go.trybeluga.ai/:shortCode', - }, - ] + return { + beforeFiles: [], + afterFiles: [ + { + source: '/favicon.ico', + destination: '/icon.svg', + }, + { + source: '/r/:shortCode', + destination: 'https://go.trybeluga.ai/:shortCode', + }, + ], + /** + * Fallback rewrites run only after every page and dynamic route has failed to match, so + * this answers a Markdown client's 404 in Markdown without shadowing any real route. + * `text/markdown` must be one of the listed media ranges, and `q=0` (RFC 9110 §12.4.2: + * not acceptable) keeps the HTML page. Next anchors both patterns to the whole header. + */ + fallback: [ + { + source: '/:path*', + has: [{ type: 'header', key: 'accept', value: '(.*,)?\\s*text/markdown\\s*([;,].*)?' }], + missing: [ + { + type: 'header', + key: 'accept', + value: + '(.*,)?\\s*text/markdown(\\s*;[^,]*?)?\\s*;\\s*q\\s*=\\s*0(\\.0{0,3})?\\s*(,.*)?', + }, + ], + destination: '/markdown-not-found', + }, + ], + } }, } diff --git a/apps/sim/proxy.test.ts b/apps/sim/proxy.test.ts index 00c1df9ad8f..860420b253a 100644 --- a/apps/sim/proxy.test.ts +++ b/apps/sim/proxy.test.ts @@ -169,7 +169,8 @@ describe('proxy matcher', () => { expect(matches('/api/desktop/tool/file')).toBe(false) }) - it('leaves the document snapshot persist unbuffered, so a whole snapshot reaches the route', () => { + it('leaves the document snapshot persists unbuffered, so a whole snapshot reaches the route', () => { + expect(matches('/api/internal/file-doc/persist')).toBe(false) expect(matches('/api/internal/project-file-doc/project-1/file-1/persist')).toBe(false) }) @@ -180,5 +181,7 @@ describe('proxy matcher', () => { expect(matches('/api/files/upload')).toBe(true) expect(matches('/api/internal/project-file-doc/project-1/file-1/seed')).toBe(true) expect(matches('/api/internal/project-file-doc/project-1/file-1/persist/other')).toBe(true) + expect(matches('/api/internal/file-doc/persist/other')).toBe(true) + expect(matches('/api/internal/file-doc/seed')).toBe(true) }) }) diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index e72a1962c6d..ad803018890 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -470,10 +470,10 @@ export const config = { '/login', '/signup', '/invite/:path*', // Match invitation routes - // Runtime CORS. The desktop's raw upload routes and the realtime relay's document persist are + // Runtime CORS. The desktop's raw upload routes and the realtime relay's document persists are // left out: running the proxy makes Next buffer the body, cutting it off at its 10 MB proxy // limit, and those bodies are whole files or document snapshots. - '/api/((?!desktop/tool/(?:import|file)$|internal/project-file-doc/[^/]+/[^/]+/persist$).*)', + '/api/((?!desktop/tool/(?:import|file)$|internal/file-doc/persist$|internal/project-file-doc/[^/]+/[^/]+/persist$).*)', // Next's image optimizer fetches public editorial images without a User-Agent. '/((?!api/|api$|_next/static|_next/image|(?:blog|changelog)/.*\\.(?:avif|gif|jpe?g|png|svg|webp)$|ingest|favicon.ico|logo/|landing/|static/|footer/|social/|enterprise/|favicon/|twitter/|robots.txt|sitemap.xml).*)', ], diff --git a/apps/sim/public/changelog/oracle-operations-v1.jpg b/apps/sim/public/changelog/oracle-operations-v1.jpg new file mode 100644 index 00000000000..7e180c75ff9 Binary files /dev/null and b/apps/sim/public/changelog/oracle-operations-v1.jpg differ diff --git a/apps/sim/public/library/best-ai-agents-ecommerce-operations-2026/cover.jpg b/apps/sim/public/library/best-ai-agents-ecommerce-operations-2026/cover.jpg new file mode 100644 index 00000000000..9e8c675ebc2 Binary files /dev/null and b/apps/sim/public/library/best-ai-agents-ecommerce-operations-2026/cover.jpg differ diff --git a/apps/sim/public/library/best-ai-agents-for-querying-sql-databases/cover.jpg b/apps/sim/public/library/best-ai-agents-for-querying-sql-databases/cover.jpg new file mode 100644 index 00000000000..62d5ba16a00 Binary files /dev/null and b/apps/sim/public/library/best-ai-agents-for-querying-sql-databases/cover.jpg differ diff --git a/apps/sim/public/library/best-discord-ai-bot-builders-2026/cover.jpg b/apps/sim/public/library/best-discord-ai-bot-builders-2026/cover.jpg new file mode 100644 index 00000000000..46c5ee6297b Binary files /dev/null and b/apps/sim/public/library/best-discord-ai-bot-builders-2026/cover.jpg differ diff --git a/apps/sim/public/library/how-to-build-whatsapp-ai-agent-with-sim/cover.jpg b/apps/sim/public/library/how-to-build-whatsapp-ai-agent-with-sim/cover.jpg new file mode 100644 index 00000000000..9f0e53be510 Binary files /dev/null and b/apps/sim/public/library/how-to-build-whatsapp-ai-agent-with-sim/cover.jpg differ diff --git a/apps/sim/scripts/test-landing-seo-e2e.ts b/apps/sim/scripts/test-landing-seo-e2e.ts index 2ea7dc57baa..bfd0f54b681 100644 --- a/apps/sim/scripts/test-landing-seo-e2e.ts +++ b/apps/sim/scripts/test-landing-seo-e2e.ts @@ -8,6 +8,16 @@ import { import integrationsJson from '@sim/deployment-config/integrations.json' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' +import { + dustProfile, + gumloopProfile, + langflowProfile, + n8nProfile, + openClawProfile, + pipedreamProfile, + vellumProfile, +} from '@/lib/compare/data' +import { INTEGRATION_SEO } from '@/app/(landing)/integrations/data/seo-content' /** * Crawls a running landing site the way a search or AI crawler does: plain GETs, no JavaScript, @@ -94,6 +104,23 @@ function jsonLdNodes(html: string): Record[] { return nodes } +/** Undoes the entity escaping React applies to text, so rendered copy compares to its source. */ +function decodeHtml(html: string): string { + return html + .replace(/'|'/g, "'") + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') +} + +/** Question text of every FAQPage entry on the page, in order. */ +function faqQuestions(html: string): string[] { + const faqPage = jsonLdNodes(html).find((node) => node['@type'] === 'FAQPage') + const entries = (faqPage?.mainEntity ?? []) as { name?: string }[] + return entries.map((entry) => entry.name ?? '') +} + /** Paths and lastmod values from the sitemap, keyed by path. */ async function readSitemap(): Promise> { const { status, body } = await get('/sitemap.xml') @@ -240,6 +267,59 @@ async function run() { assert(String(product['@id']).endsWith('#software'), `Offers sit on ${product['@id']}`) }) + await check('comparison pages lead with their answer and their tracked-prompt FAQs', async () => { + const failures: string[] = [] + for (const profile of [ + n8nProfile, + dustProfile, + gumloopProfile, + vellumProfile, + langflowProfile, + pipedreamProfile, + openClawProfile, + ]) { + const { status, body } = await get(`/comparisons/${profile.id}`) + if (status !== 200) { + failures.push(`${profile.id}: status ${status}`) + continue + } + const text = decodeHtml(body) + const lead = profile.leadAnswer?.map((part) => (typeof part === 'string' ? part : part.text)) + const firstSection = text.indexOf(' firstSection) { + failures.push(`${profile.id}: lead answer missing or below the first section`) + } + const expected = (profile.faqs ?? []).map((faq) => faq.question) + const questions = faqQuestions(body).slice(0, expected.length) + if (expected.length === 0 || questions.join('|') !== expected.join('|')) { + failures.push(`${profile.id}: FAQPage does not open with the profile FAQs`) + } + } + assert.deepEqual(failures, []) + }) + + await check('rebuilt integration pages render their comparison and FAQs', async () => { + const failures: string[] = [] + for (const slug of ['github', 'shopify', 'snowflake']) { + const seo = INTEGRATION_SEO[slug] + const { status, body } = await get(`/integrations/${slug}`) + if (status !== 200) { + failures.push(`${slug}: status ${status}`) + continue + } + const text = decodeHtml(body) + if (!seo?.comparison || !text.includes(seo.comparison.heading)) { + failures.push(`${slug}: comparison section missing`) + } + const expected = (seo?.faqs ?? []).map((faq) => faq.question) + if (expected.length === 0 || faqQuestions(body).join('|') !== expected.join('|')) { + failures.push(`${slug}: FAQPage does not match the hand-written FAQs`) + } + } + assert.deepEqual(failures, []) + }) + await check('blog, library, and changelog advertise their RSS feeds', async () => { const missing: string[] = [] for (const [path, feed] of [ diff --git a/apps/sim/scripts/test-mcp-host-e2e.ts b/apps/sim/scripts/test-mcp-host-e2e.ts index e2632a88f05..96928c60db0 100644 --- a/apps/sim/scripts/test-mcp-host-e2e.ts +++ b/apps/sim/scripts/test-mcp-host-e2e.ts @@ -6,8 +6,8 @@ import { getErrorMessage } from '@sim/utils/errors' import { SIM_DOCS_URL } from '@sim/utils/site' /** - * Exercises the public ownership challenge, host isolation, the RFC 9727 API catalog, and the - * app-wide JSON 404 through a running local app. + * Exercises the public ownership challenge, host isolation, the RFC 9727 API catalog, the + * app-wide JSON 404, and the Markdown 404 for `Accept: text/markdown` through a running local app. */ const logger = createLogger('McpHostE2E') const CHALLENGE_PATH = '/.well-known/openai-apps-challenge' @@ -17,6 +17,8 @@ const PROTECTED_RESOURCE_PATH = '/.well-known/oauth-protected-resource' const API_CATALOG_PATH = '/.well-known/api-catalog' const API_CATALOG_CONTENT_TYPE = 'application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"' +const MARKDOWN_ACCEPT = 'text/markdown' +const BROWSER_ACCEPT = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' const startedAt = new Date().toISOString() const configuredBaseUrl = process.env.MCP_HOST_E2E_BASE_URL @@ -43,18 +45,25 @@ const requests: { path: string userAgent: string host: string + accept: string status: number }[] = [] -async function request(path: string, userAgent: string, host = MCP_HOST, method = 'GET') { +async function request( + path: string, + userAgent: string, + host = MCP_HOST, + method = 'GET', + accept = '*/*' +) { // boundary-raw-fetch: exercise the real proxy and route over local HTTP. const response = await fetch(new URL(path, baseUrl), { method, - headers: { Host: host, 'User-Agent': userAgent }, + headers: { Host: host, 'User-Agent': userAgent, Accept: accept }, redirect: 'error', signal: AbortSignal.timeout(120_000), }) - requests.push({ method, path, userAgent, host, status: response.status }) + requests.push({ method, path, userAgent, host, accept, status: response.status }) const body = await response.text() return { response, body } } @@ -122,7 +131,11 @@ try { response.headers.get('link'), `<${baseUrl.origin}${API_CATALOG_PATH}>; rel="api-catalog"` ) - const [api] = JSON.parse(body).linkset + const [catalog, api] = JSON.parse(body).linkset + assert.deepEqual(catalog, { + anchor: `${baseUrl.origin}${API_CATALOG_PATH}`, + item: [{ href: `${baseUrl.origin}/api/v2` }], + }) assert.equal(api.anchor, `${baseUrl.origin}/api/v2`) assert.equal(api['service-desc'][0].href, `${SIM_DOCS_URL}/openapi.json`) assert.equal(api.status[0].href, `${baseUrl.origin}/api/health`) @@ -138,6 +151,79 @@ try { assert.match(response.headers.get('link') ?? '', /rel="api-catalog"$/) }) + for (const path of ['/not-a-real-page', '/not-a-real/nested/page']) { + await check(`unknown page ${path} answers a Markdown client in Markdown`, async () => { + const { response, body } = await request( + path, + 'python-requests/2.32.3', + baseUrl.host, + 'GET', + MARKDOWN_ACCEPT + ) + assert.equal(response.status, 404) + assert.equal(response.headers.get('content-type'), 'text/markdown; charset=utf-8') + assert.match(response.headers.get('vary') ?? '', /\bAccept\b/) + assert.match(response.headers.get('cache-control') ?? '', /\bno-store\b/) + assert.match(body, /^# Page not found\n/) + assert.match(body, /\/llms\.txt\)/) + }) + } + + for (const method of ['POST', 'OPTIONS']) { + await check(`an unknown page answers a Markdown ${method} in Markdown`, async () => { + const { response } = await request( + '/not-a-real-page', + 'python-requests/2.32.3', + baseUrl.host, + method, + MARKDOWN_ACCEPT + ) + assert.equal(response.status, 404) + assert.equal(response.headers.get('content-type'), 'text/markdown; charset=utf-8') + }) + } + + await check('a client that refuses Markdown with q=0 gets the HTML 404', async () => { + const { response } = await request( + '/not-a-real-page', + 'python-requests/2.32.3', + baseUrl.host, + 'GET', + 'text/html, text/markdown;q=0' + ) + assert.equal(response.status, 404) + assert.match(response.headers.get('content-type') ?? '', /^text\/html/) + }) + + await check('unknown page still answers a browser with the HTML 404', async () => { + const { response } = await request( + '/not-a-real-page', + 'Mozilla/5.0', + baseUrl.host, + 'GET', + BROWSER_ACCEPT + ) + assert.equal(response.status, 404) + assert.match(response.headers.get('content-type') ?? '', /^text\/html/) + }) + + await check('a real page is not shadowed by the Markdown 404', async () => { + const { response } = await request('/', 'Mozilla/5.0', baseUrl.host, 'GET', MARKDOWN_ACCEPT) + assert.equal(response.status, 200) + }) + + await check('an unknown API path keeps its JSON 404 for a Markdown client', async () => { + const { response, body } = await request( + '/api/not-a-real-route', + 'Mozilla/5.0', + baseUrl.host, + 'GET', + MARKDOWN_ACCEPT + ) + assert.equal(response.status, 404) + assert.deepEqual(JSON.parse(body), { error: 'Not found' }) + }) + await check('application host still serves health checks', async () => { const { response } = await request('/api/health', 'Mozilla/5.0', baseUrl.host) assert.equal(response.status, 200) diff --git a/knip.jsonc b/knip.jsonc index 640b77d6370..99112b5952e 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -64,7 +64,6 @@ "lib/api/contracts/project-file-extraction.ts": ["exports", "types"], "lib/api/contracts/v2/project-file-downloads.ts": ["exports", "types"], "lib/api/contracts/v2/project-file-extraction.ts": ["exports", "types"], - "lib/api/contracts/v2/project-file-search.ts": ["exports", "types"], "components/mcp/index.ts": ["files"], "triggers/quickbooks/index.ts": ["files"], "lib/api/contracts/selectors/index.ts": ["files"], diff --git a/packages/db/large-table-vacuum-throttle-migration.integration.ts b/packages/db/large-table-vacuum-throttle-migration.integration.ts new file mode 100644 index 00000000000..638ea10b49d --- /dev/null +++ b/packages/db/large-table-vacuum-throttle-migration.integration.ts @@ -0,0 +1,144 @@ +import { readFile } from 'node:fs/promises' +import { applyMigration } from '@sim/db/scripts/migration-fixture' +import { readTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' +import { sleep } from '@sim/utils/helpers' +import { generateId } from '@sim/utils/id' +import postgres, { type Sql } from 'postgres' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +const databaseUrl = readTestDatabaseUrl() +const TABLES = ['embedding', 'embedding_search', 'workflow_execution_logs'] as const +const THROTTLE = ['autovacuum_vacuum_cost_delay=10', 'autovacuum_vacuum_cost_limit=200'] + +/** + * Applies 0431 to stand-in tables, each with a TOAST relation, and checks the settings land on + * both relations without the statement ever holding a lock that reads or writes wait on. + */ +describe('large table vacuum throttle migration in PostgreSQL', () => { + let admin: Sql + let migrator: Sql + let writer: Sql + let migration: string + let statements: string[] + const schema = `vacuum_throttle_${generateId().replaceAll('-', '')}` + const rel = (table: string) => `"${schema}"."${table}"` + + /** The writer gives up quickly, so a write queued behind the migration fails instead of hanging. */ + const connect = (lockTimeoutMs?: number) => + postgres(databaseUrl, { + max: 1, + connection: { + search_path: schema, + ...(lockTimeoutMs ? { lock_timeout: lockTimeoutMs } : {}), + }, + onnotice: () => undefined, + }) + + beforeAll(async () => { + admin = postgres(databaseUrl, { max: 1, onnotice: () => undefined }) + await admin.unsafe( + [ + `CREATE SCHEMA "${schema}"`, + ...TABLES.map((table) => `CREATE TABLE ${rel(table)} (id text PRIMARY KEY, body text)`), + ].join(';') + ) + migrator = connect() + writer = connect(300) + migration = await readFile( + new URL('./migrations/0431_large_table_vacuum_throttle.sql', import.meta.url), + 'utf8' + ) + statements = migration + .split('--> statement-breakpoint') + .map((statement) => statement.trim()) + .filter(Boolean) + }) + + afterAll(async () => { + await migrator?.end() + await writer?.end() + if (admin) { + await admin.unsafe(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`) + await admin.end() + } + }) + + it('takes only SHARE UPDATE EXCLUSIVE and rewrites nothing, so reads and writes proceed', async () => { + const relfilenode = async () => + ( + await admin<{ node: string }[]>` + SELECT relfilenode::text AS node FROM pg_class WHERE oid = ${rel('embedding')}::regclass` + )[0]?.node + const before = await relfilenode() + + await migrator.begin(async (tx) => { + await tx.unsafe(statements[0]) + const locks = await tx<{ mode: string }[]>` + SELECT mode FROM pg_locks + WHERE pid = pg_backend_pid() AND locktype = 'relation' + AND relation = ${rel('embedding')}::regclass` + expect(locks.map((lock) => lock.mode)).toEqual(['ShareUpdateExclusiveLock']) + + // Another session reads and writes the table while the migration transaction holds it. + await writer`INSERT INTO embedding (id, body) VALUES ('during', 'x')` + expect(await writer`SELECT id FROM embedding`).toHaveLength(1) + }) + + expect(await relfilenode()).toBe(before) + }) + + it('does not queue writers behind it while it waits for a conflicting holder', async () => { + const holder = connect() + let releaseHolder = () => {} + const released = new Promise((resolve) => { + releaseHolder = resolve + }) + try { + let holderLocked = () => {} + const locked = new Promise((resolve) => { + holderLocked = resolve + }) + const holding = holder.begin(async (tx) => { + await tx.unsafe('LOCK TABLE embedding_search IN SHARE UPDATE EXCLUSIVE MODE') + holderLocked() + await released + }) + await locked + + // postgres.js sends a query only once it is awaited or executed. + const waiting = migrator.unsafe(statements[1]).execute() + let queued: { mode: string }[] = [] + for (let attempt = 0; attempt < 100 && queued.length === 0; attempt++) { + queued = await admin<{ mode: string }[]>` + SELECT mode FROM pg_locks + WHERE NOT granted AND locktype = 'relation' + AND relation = ${rel('embedding_search')}::regclass` + if (queued.length === 0) await sleep(10) + } + expect(queued.map((lock) => lock.mode)).toEqual(['ShareUpdateExclusiveLock']) + + await writer`INSERT INTO embedding_search (id, body) VALUES ('queued', 'x')` + + releaseHolder() + await holding + await waiting + } finally { + releaseHolder() + await holder.end() + } + }) + + it('sets the throttle on every table and its TOAST relation', async () => { + // The whole file, so this holds on its own; reapplying the same settings is a no-op. + await applyMigration(migrator, migration) + const rows = await admin<{ heap: string[] | null; toast: string[] | null }[]>` + SELECT c.reloptions AS heap, t.reloptions AS toast + FROM pg_class c JOIN pg_class t ON t.oid = c.reltoastrelid + WHERE c.oid = ANY(${TABLES.map(rel)}::regclass[])` + expect(rows).toHaveLength(TABLES.length) + for (const row of rows) { + expect(row.heap).toEqual(expect.arrayContaining(THROTTLE)) + expect(row.toast).toEqual(expect.arrayContaining(THROTTLE)) + } + }) +}) diff --git a/packages/db/migrations/0431_large_table_vacuum_throttle.sql b/packages/db/migrations/0431_large_table_vacuum_throttle.sql new file mode 100644 index 00000000000..c09272b77d5 --- /dev/null +++ b/packages/db/migrations/0431_large_table_vacuum_throttle.sql @@ -0,0 +1,9 @@ +-- migration-safe: table-local maintenance settings only; a catalog update under SHARE UPDATE EXCLUSIVE, which reads and writes do not wait on, with no row rewrite. +-- Vacuuming these tables writes WAL, mostly full-page images, faster than the synchronous standbys +-- flush it, so every write commit on the primary waits on a standby for as long as the vacuum runs. +-- A per-table cost delay and limit cap that rate. This reverses 0362's choice for these tables +-- only: they leave cross-worker I/O balancing and vacuum more slowly, the lesser cost here. The +-- TOAST values would be inherited, but are set explicitly because the TOAST vacuums are the largest. +ALTER TABLE "embedding" SET (autovacuum_vacuum_cost_delay = 10, autovacuum_vacuum_cost_limit = 200, toast.autovacuum_vacuum_cost_delay = 10, toast.autovacuum_vacuum_cost_limit = 200);--> statement-breakpoint +ALTER TABLE "embedding_search" SET (autovacuum_vacuum_cost_delay = 10, autovacuum_vacuum_cost_limit = 200, toast.autovacuum_vacuum_cost_delay = 10, toast.autovacuum_vacuum_cost_limit = 200);--> statement-breakpoint +ALTER TABLE "workflow_execution_logs" SET (autovacuum_vacuum_cost_delay = 10, autovacuum_vacuum_cost_limit = 200, toast.autovacuum_vacuum_cost_delay = 10, toast.autovacuum_vacuum_cost_limit = 200); diff --git a/packages/db/migrations/0432_tool_execution_deadline.sql b/packages/db/migrations/0432_tool_execution_deadline.sql new file mode 100644 index 00000000000..441fec882e8 --- /dev/null +++ b/packages/db/migrations/0432_tool_execution_deadline.sql @@ -0,0 +1 @@ +ALTER TABLE "copilot_async_tool_calls" ADD COLUMN "execution_deadline_at" timestamp with time zone; \ No newline at end of file diff --git a/packages/db/migrations/meta/0431_snapshot.json b/packages/db/migrations/meta/0431_snapshot.json new file mode 100644 index 00000000000..7764b317201 --- /dev/null +++ b/packages/db/migrations/meta/0431_snapshot.json @@ -0,0 +1,31432 @@ +{ + "id": "5c28fb1c-241a-4cdb-9061-480367c42f4a", + "prevId": "5a367f45-20c0-4cd5-8a48-115ad19079f4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_at": { + "name": "refresh_revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_code": { + "name": "refresh_revoked_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_token_hash": { + "name": "refresh_revoked_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_memory_turn": { + "name": "agent_memory_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_order": { + "name": "execution_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_memory_turn_invocation_unique": { + "name": "agent_memory_turn_invocation_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memory_turn_workflow_idx": { + "name": "agent_memory_turn_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memory_turn_memory_id_memory_id_fk": { + "name": "agent_memory_turn_memory_id_memory_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memory_turn_workflow_id_workflow_id_fk": { + "name": "agent_memory_turn_workflow_id_workflow_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_change": { + "name": "changelog_change", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "changelog_change_release_position_unique": { + "name": "changelog_change_release_position_unique", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changelog_change_release_id_changelog_release_id_fk": { + "name": "changelog_change_release_id_changelog_release_id_fk", + "tableFrom": "changelog_change", + "tableTo": "changelog_release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changelog_change_workflow_id_workflow_id_fk": { + "name": "changelog_change_workflow_id_workflow_id_fk", + "tableFrom": "changelog_change", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_change_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "changelog_change_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "changelog_change", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_change_chat_id_copilot_chats_id_fk": { + "name": "changelog_change_chat_id_copilot_chats_id_fk", + "tableFrom": "changelog_change", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_release": { + "name": "changelog_release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_major": { + "name": "version_major", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_minor": { + "name": "version_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_patch": { + "name": "version_patch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bump_reason": { + "name": "bump_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_file_id": { + "name": "body_file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "published_at": { + "name": "published_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "changelog_release_workspace_version_unique": { + "name": "changelog_release_workspace_version_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_major", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_minor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_patch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changelog_release_body_file_unique": { + "name": "changelog_release_body_file_unique", + "columns": [ + { + "expression": "body_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changelog_release_workspace_published_idx": { + "name": "changelog_release_workspace_published_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changelog_release_workspace_id_workspace_id_fk": { + "name": "changelog_release_workspace_id_workspace_id_fk", + "tableFrom": "changelog_release", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changelog_release_body_file_id_workspace_files_id_fk": { + "name": "changelog_release_body_file_id_workspace_files_id_fk", + "tableFrom": "changelog_release", + "tableTo": "workspace_files", + "columnsFrom": ["body_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "changelog_release_created_by_user_id_fk": { + "name": "changelog_release_created_by_user_id_fk", + "tableFrom": "changelog_release", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_release_updated_by_user_id_fk": { + "name": "changelog_release_updated_by_user_id_fk", + "tableFrom": "changelog_release", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "changelog_release_version_check": { + "name": "changelog_release_version_check", + "value": "\"changelog_release\".\"version_major\" >= 0 AND \"changelog_release\".\"version_minor\" >= 0 AND \"changelog_release\".\"version_patch\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_user_id_idx": { + "name": "chat_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_credential_token": { + "name": "client_credential_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_digest": { + "name": "access_token_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "client_credential_token_expires_at_idx": { + "name": "client_credential_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_credential_token\".\"access_token_digest\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_credential_token_access_token_digest_idx": { + "name": "client_credential_token_access_token_digest_idx", + "columns": [ + { + "expression": "access_token_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_credential_token\".\"access_token_digest\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_requested_at": { + "name": "permission_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pickup_deadline_at": { + "name": "pickup_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "persist_seq": { + "name": "persist_seq", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": "nextval('copilot_async_tool_calls_persist_seq')" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "browser_download_started_at": { + "name": "browser_download_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_started_at": { + "name": "execution_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_settled_at": { + "name": "execution_settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_owner_token": { + "name": "execution_owner_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_lease_expires_at": { + "name": "execution_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_revoked_at": { + "name": "execution_revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_attempts": { + "name": "execution_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "client_workflow_execution_id": { + "name": "client_workflow_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_processes": { + "name": "sandbox_processes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memory_space_id": { + "name": "memory_space_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_key": { + "name": "external_conversation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_metadata": { + "name": "external_conversation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_organization_id_idx": { + "name": "copilot_chats_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_external_conversation_unique": { + "name": "copilot_chats_external_conversation_unique", + "columns": [ + { + "expression": "external_conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"copilot_chats\".\"external_conversation_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_org_created_idx": { + "name": "copilot_chats_user_org_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_organization_id_organization_id_fk": { + "name": "copilot_chats_organization_id_organization_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "copilot_chats_owner_check": { + "name": "copilot_chats_owner_check", + "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1" + }, + "copilot_chats_organization_workflow_check": { + "name": "copilot_chats_organization_workflow_check", + "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "display_content": { + "name": "display_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_organization_request_stops": { + "name": "copilot_organization_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_organization_request_stops_user_id_user_id_fk": { + "name": "copilot_organization_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_organization_request_stops_organization_id_organization_id_fk": { + "name": "copilot_organization_request_stops_organization_id_organization_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_organization_request_stops_user_id_organization_id_stream_id_pk": { + "name": "copilot_organization_request_stops_user_id_organization_id_stream_id_pk", + "columns": ["user_id", "organization_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_request_stops": { + "name": "copilot_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_request_stops_user_id_user_id_fk": { + "name": "copilot_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_request_stops_workspace_id_workspace_id_fk": { + "name": "copilot_request_stops_workspace_id_workspace_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_request_stops_user_id_workspace_id_stream_id_pk": { + "name": "copilot_request_stops_user_id_workspace_id_stream_id_pk", + "columns": ["user_id", "workspace_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_execution_version": { + "name": "tool_execution_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_admission_closed_at": { + "name": "tool_admission_closed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "desktop_device_id": { + "name": "desktop_device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_started_at_idx": { + "name": "copilot_runs_chat_started_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_desktop_device_started_at_idx": { + "name": "copilot_runs_desktop_device_started_at_idx", + "columns": [ + { + "expression": "desktop_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_runs\".\"desktop_device_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_organization_id_organization_id_fk": { + "name": "copilot_runs_organization_id_organization_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_desktop_device_id_desktop_devices_id_fk": { + "name": "copilot_runs_desktop_device_id_desktop_devices_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "desktop_devices", + "columnsFrom": ["desktop_device_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_service_usage": { + "name": "copilot_service_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": false + }, + "worker_origin": { + "name": "worker_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_service_usage_pending_idx": { + "name": "copilot_service_usage_pending_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "delivered_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_personal_token": { + "name": "encrypted_personal_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_oauth_config_version": { + "name": "mcp_oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tools": { + "name": "mcp_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "mcp_tools_refreshed_at": { + "name": "mcp_tools_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_organization_id_idx": { + "name": "credential_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_organization_account_unique": { + "name": "credential_organization_account_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_org_personal_token_unique": { + "name": "credential_org_personal_token_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_mcp_server_idx": { + "name": "credential_mcp_server_idx", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_unique": { + "name": "credential_group_option_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_managed_mcp_enrollment_server_unique": { + "name": "credential_managed_mcp_enrollment_server_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_mcp'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_personal_token_identity_unique": { + "name": "credential_personal_token_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_organization_id_organization_id_fk": { + "name": "credential_organization_id_organization_id_fk", + "tableFrom": "credential", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_slack_app_id_slack_app_id_fk": { + "name": "credential_slack_app_id_slack_app_id_fk", + "tableFrom": "credential", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_mcp_server_id_mcp_servers_id_fk": { + "name": "credential_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "credential", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_owner_check": { + "name": "credential_owner_check", + "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1" + }, + "credential_organization_type_check": { + "name": "credential_organization_type_check", + "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')" + }, + "credential_personal_token_source_check": { + "name": "credential_personal_token_source_check", + "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_managed_mcp_source_check": { + "name": "credential_managed_mcp_source_check", + "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND provider_subject_id IS NULL\n AND provider_tenant_id IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_creator_source_check": { + "name": "credential_creator_source_check", + "value": "(type::text = 'managed_mcp') OR created_by IS NOT NULL" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_organization_id_idx": { + "name": "credential_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_organization_unique": { + "name": "credential_group_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_unique": { + "name": "credential_group_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_organization_id_organization_id_fk": { + "name": "credential_group_organization_id_organization_id_fk", + "tableFrom": "credential_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_owner_check": { + "name": "credential_group_owner_check", + "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_enrollment_group_user_unique": { + "name": "credential_group_enrollment_group_user_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_user_id_idx": { + "name": "credential_group_enrollment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_user_id_user_id_fk": { + "name": "credential_group_enrollment_user_id_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dashboard_workspace_id_unique": { + "name": "dashboard_workspace_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboard_created_by_user_id_fk": { + "name": "dashboard_created_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "dashboard_updated_by_user_id_fk": { + "name": "dashboard_updated_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.desktop_devices": { + "name": "desktop_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_version": { + "name": "app_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "desktop_devices_user_id_idx": { + "name": "desktop_devices_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "desktop_devices_session_id_idx": { + "name": "desktop_devices_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "desktop_devices_user_id_user_id_fk": { + "name": "desktop_devices_user_id_user_id_fk", + "tableFrom": "desktop_devices", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "desktop_devices_session_id_session_id_fk": { + "name": "desktop_devices_session_id_session_id_fk", + "tableFrom": "desktop_devices", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_queue_token": { + "name": "processing_queue_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_deferred_until": { + "name": "processing_deferred_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_recovery_after": { + "name": "processing_recovery_after", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{ws}'::text[]" + }, + "acl_requirements": { + "name": "acl_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "acl_verified_at": { + "name": "acl_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_modified_at": { + "name": "source_modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_seen_at": { + "name": "source_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_uploaded_by_idx": { + "name": "doc_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"uploaded_by\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_kb_source_modified_idx": { + "name": "doc_kb_source_modified_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_modified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_acl_gin_idx": { + "name": "doc_acl_gin_idx", + "columns": [ + { + "expression": "acl", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "array_ops" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_processing_recovery_idx": { + "name": "doc_connector_processing_recovery_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_connector_processing_status_idx": { + "name": "doc_connector_processing_status_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_source_lookup_idx": { + "name": "doc_connector_source_lookup_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_reconciliation_v2_idx": { + "name": "doc_connector_reconciliation_v2_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_tombstone_idx": { + "name": "doc_connector_tombstone_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NULL AND (\"document\".\"deleted_at\" IS NOT NULL OR \"document\".\"content_hash\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_live_idx": { + "name": "doc_connector_live_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag1_lower_idx": { + "name": "doc_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag2_lower_idx": { + "name": "doc_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag3_lower_idx": { + "name": "doc_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag4_lower_idx": { + "name": "doc_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag5_lower_idx": { + "name": "doc_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag6_lower_idx": { + "name": "doc_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag7_lower_idx": { + "name": "doc_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "doc_acl_token_shape_check": { + "name": "doc_acl_token_shape_check", + "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))" + } + }, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_384": { + "name": "embedding_384", + "type": "vector(384)", + "primaryKey": false, + "notNull": false + }, + "embedding_768": { + "name": "embedding_768", + "type": "vector(768)", + "primaryKey": false, + "notNull": false + }, + "embedding_1024": { + "name": "embedding_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "embedding_3072": { + "name": "embedding_3072", + "type": "vector(3072)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag1_lower_idx": { + "name": "emb_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag2_lower_idx": { + "name": "emb_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag3_lower_idx": { + "name": "emb_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag4_lower_idx": { + "name": "emb_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag5_lower_idx": { + "name": "emb_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag6_lower_idx": { + "name": "emb_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag7_lower_idx": { + "name": "emb_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": { + "fastupdate": "off" + } + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_width_check": { + "name": "embedding_width_check", + "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_search": { + "name": "embedding_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "binary": { + "name": "binary", + "type": "bit(1536)", + "primaryKey": false, + "notNull": false + }, + "binary_384": { + "name": "binary_384", + "type": "bit(384)", + "primaryKey": false, + "notNull": false + }, + "binary_768": { + "name": "binary_768", + "type": "bit(768)", + "primaryKey": false, + "notNull": false + }, + "binary_1024": { + "name": "binary_1024", + "type": "bit(1024)", + "primaryKey": false, + "notNull": false + }, + "binary_3072": { + "name": "binary_3072", + "type": "bit(3072)", + "primaryKey": false, + "notNull": false + }, + "vector": { + "name": "vector", + "type": "halfvec(1536)", + "primaryKey": false, + "notNull": false + }, + "vector_384": { + "name": "vector_384", + "type": "halfvec(384)", + "primaryKey": false, + "notNull": false + }, + "vector_512": { + "name": "vector_512", + "type": "halfvec(512)", + "primaryKey": false, + "notNull": false + }, + "vector_768": { + "name": "vector_768", + "type": "halfvec(768)", + "primaryKey": false, + "notNull": false + }, + "vector_1024": { + "name": "vector_1024", + "type": "halfvec(1024)", + "primaryKey": false, + "notNull": false + }, + "vector_3072": { + "name": "vector_3072", + "type": "halfvec(3072)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "embedding_search_kb_idx": { + "name": "embedding_search_kb_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_search_document_lookup_idx": { + "name": "embedding_search_document_lookup_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"embedding_search\".\"enabled\"", + "concurrently": true, + "method": "btree", + "with": {} + }, + "embedding_search_cosine_hnsw_idx": { + "name": "embedding_search_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_512_cosine_hnsw_idx": { + "name": "embedding_search_512_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_512", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_384_cosine_hnsw_idx": { + "name": "embedding_search_384_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_768_cosine_hnsw_idx": { + "name": "embedding_search_768_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_1024_cosine_hnsw_idx": { + "name": "embedding_search_1024_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_3072_cosine_hnsw_idx": { + "name": "embedding_search_3072_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_3072", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + } + }, + "foreignKeys": { + "embedding_search_id_embedding_id_fk": { + "name": "embedding_search_id_embedding_id_fk", + "tableFrom": "embedding_search", + "tableTo": "embedding", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_search_width_check": { + "name": "embedding_search_width_check", + "value": "num_nonnulls(\"binary\", \"binary_384\", \"binary_768\", \"binary_1024\", \"binary_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workspace_id_idx": { + "name": "execution_large_values_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_search_dependency": { + "name": "file_search_dependency", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "file_search_dependency_file_idx": { + "name": "file_search_dependency_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "file_search_dependency_owner_idx": { + "name": "file_search_dependency_owner_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_search_dependency_build_id_workspace_file_search_build_id_fk": { + "name": "file_search_dependency_build_id_workspace_file_search_build_id_fk", + "tableFrom": "file_search_dependency", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_search_dependency_build_id_file_id_pk": { + "name": "file_search_dependency_build_id_file_id_pk", + "columns": ["build_id", "file_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_search_dispatch_queue": { + "name": "file_search_dispatch_queue", + "schema": "", + "columns": { + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_search_dispatch_schedule_idx": { + "name": "file_search_dispatch_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "file_search_dispatch_queue_entity_type_entity_id_pk": { + "name": "file_search_dispatch_queue_entity_type_entity_id_pk", + "columns": ["entity_type", "entity_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_dispatch_owner_check": { + "name": "file_search_dispatch_owner_check", + "value": "\"file_search_dispatch_queue\".\"entity_type\" IN ('workspace', 'project') AND length(\"file_search_dispatch_queue\".\"entity_id\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_project_id_idx": { + "name": "folder_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "folder_project_resource_parent_name_active_unique": { + "name": "folder_project_resource_parent_name_active_unique", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL AND \"folder\".\"project_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_project_id_project_id_fk": { + "name": "folder_project_id_project_id_fk", + "tableFrom": "folder", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "folder_workspace_parent_owner_fk": { + "name": "folder_workspace_parent_owner_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id", "workspace_id", "resource_type"], + "columnsTo": ["id", "workspace_id", "resource_type"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folder_project_parent_owner_fk": { + "name": "folder_project_parent_owner_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id", "project_id", "resource_type"], + "columnsTo": ["id", "project_id", "resource_type"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "folder_workspace_owner_type_unique": { + "name": "folder_workspace_owner_type_unique", + "nullsNotDistinct": false, + "columns": ["id", "workspace_id", "resource_type"] + }, + "folder_project_owner_type_unique": { + "name": "folder_project_owner_type_unique", + "nullsNotDistinct": false, + "columns": ["id", "project_id", "resource_type"] + } + }, + "policies": {}, + "checkConstraints": { + "folder_owner_check": { + "name": "folder_owner_check", + "value": "num_nonnulls(\"folder\".\"workspace_id\", \"folder\".\"project_id\") = 1 AND (\"folder\".\"project_id\" IS NULL OR \"folder\".\"resource_type\" = 'file')" + } + }, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_search_index": { + "name": "is_search_index", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_organization_id_idx": { + "name": "kb_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_search_index_unique": { + "name": "kb_organization_search_index_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_name_active_unique": { + "name": "kb_organization_name_active_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_search_index_unique": { + "name": "kb_workspace_search_index_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_organization_id_organization_id_fk": { + "name": "knowledge_base_organization_id_organization_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kb_owner_check": { + "name": "kb_owner_check", + "value": "num_nonnulls(\"knowledge_base\".\"workspace_id\", \"knowledge_base\".\"organization_id\") = 1" + }, + "kb_organization_search_index_check": { + "name": "kb_organization_search_index_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"is_search_index\"" + }, + "kb_organization_folder_check": { + "name": "kb_organization_folder_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"folder_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "access_mode": { + "name": "access_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workspace'" + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_status": { + "name": "member_sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "member_sync_lock_token": { + "name": "member_sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_lock_lease_at": { + "name": "member_sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_member_sync_at": { + "name": "next_member_sync_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_at": { + "name": "last_member_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_error": { + "name": "last_member_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_consecutive_failures": { + "name": "member_sync_consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "access_rewrite_pending": { + "name": "access_rewrite_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image_removal_pending": { + "name": "image_removal_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "member_tombstone_cursor": { + "name": "member_tombstone_cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "member_resurrection_cursor": { + "name": "member_resurrection_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "directory_checkpoint": { + "name": "directory_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "next_directory_sync_at": { + "name": "next_directory_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detached_at": { + "name": "detached_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detach_reserved_bytes": { + "name": "detach_reserved_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_member_sync_due_idx": { + "name": "kc_member_sync_due_idx", + "columns": [ + { + "expression": "member_sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_member_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_directory_sync_due_idx": { + "name": "kc_directory_sync_due_idx", + "columns": [ + { + "expression": "next_directory_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_credential_group_id_credential_group_id_fk": { + "name": "knowledge_connector_credential_group_id_credential_group_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kc_access_mode_check": { + "name": "kc_access_mode_check", + "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')" + }, + "kc_member_sync_status_check": { + "name": "kc_member_sync_status_check", + "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')" + }, + "kc_sync_lock_exclusive_check": { + "name": "kc_sync_lock_exclusive_check", + "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member": { + "name": "knowledge_connector_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_listing_at": { + "name": "last_complete_listing_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_listed_count": { + "name": "last_listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_synced_through": { + "name": "member_synced_through", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewed_at": { + "name": "scope_renewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_cursor": { + "name": "scope_renewal_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_started_at": { + "name": "scope_renewal_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "change_cursor": { + "name": "change_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kcm_organization_id_idx": { + "name": "kcm_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_credential_unique": { + "name": "kcm_connector_credential_unique", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_queue_idx": { + "name": "kcm_connector_queue_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "last_started_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_credential_idx": { + "name": "kcm_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_workspace_id_workspace_id_fk": { + "name": "knowledge_connector_member_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_organization_id_organization_id_fk": { + "name": "knowledge_connector_member_organization_id_organization_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_credential_id_credential_id_fk": { + "name": "knowledge_connector_member_credential_id_credential_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcm_owner_check": { + "name": "kcm_owner_check", + "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1" + }, + "kcm_status_check": { + "name": "kcm_status_check", + "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')" + }, + "kcm_subject_token_shape_check": { + "name": "kcm_subject_token_shape_check", + "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member_sync_log": { + "name": "knowledge_connector_member_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "members_claimed": { + "name": "members_claimed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_completed": { + "name": "members_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_incomplete": { + "name": "members_incomplete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_failed": { + "name": "members_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processing_dispatch_failed": { + "name": "processing_dispatch_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "docs_listed": { + "name": "docs_listed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_hydrated_once": { + "name": "docs_hydrated_once", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_added": { + "name": "observations_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_renewed": { + "name": "observations_renewed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_removed": { + "name": "observations_removed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_tombstoned": { + "name": "docs_tombstoned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_resurrected": { + "name": "docs_resurrected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_purged": { + "name": "docs_purged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credentials_audited": { + "name": "credentials_audited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_failure_class": { + "name": "database_failure_class", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcmsl_connector_started_at_idx": { + "name": "kcmsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcmsl_started_at_partial_idx": { + "name": "kcmsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcmsl_status_check": { + "name": "kcmsl_status_check", + "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')" + }, + "kcmsl_database_failure_class_check": { + "name": "kcmsl_database_failure_class_check", + "value": "\"knowledge_connector_member_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_partition": { + "name": "knowledge_connector_partition", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "partition_key": { + "name": "partition_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_at": { + "name": "retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_served_at": { + "name": "last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure": { + "name": "failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "permission_cursor": { + "name": "permission_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_attempts": { + "name": "permission_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "permission_retry_at": { + "name": "permission_retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permission_last_served_at": { + "name": "permission_last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_started_at": { + "name": "permission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_failure": { + "name": "permission_failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcp_content_due_idx": { + "name": "kcp_content_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcp_permission_due_idx": { + "name": "kcp_permission_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_partition_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_partition_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_partition", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcp_pk": { + "name": "kcp_pk", + "columns": ["connector_id", "partition_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcp_partition_key_check": { + "name": "kcp_partition_key_check", + "value": "octet_length(\"knowledge_connector_partition\".\"partition_key\") BETWEEN 1 AND 1024" + }, + "kcp_context_check": { + "name": "kcp_context_check", + "value": "jsonb_typeof(\"knowledge_connector_partition\".\"context\") = 'object' AND octet_length(\"knowledge_connector_partition\".\"context\"::text) <= 16384" + }, + "kcp_status_check": { + "name": "kcp_status_check", + "value": "\"knowledge_connector_partition\".\"status\" IN ('pending', 'complete', 'blocked')" + }, + "kcp_cursor_check": { + "name": "kcp_cursor_check", + "value": "(\"knowledge_connector_partition\".\"cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"cursor\") <= 393216) AND (\"knowledge_connector_partition\".\"permission_cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"permission_cursor\") <= 393216)" + }, + "kcp_attempts_check": { + "name": "kcp_attempts_check", + "value": "\"knowledge_connector_partition\".\"attempts\" >= 0 AND \"knowledge_connector_partition\".\"permission_attempts\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_grant": { + "name": "knowledge_connector_permission_grant", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kcpg_subject_idx": { + "name": "kcpg_subject_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kcpg_snapshot_fk": { + "name": "kcpg_snapshot_fk", + "tableFrom": "knowledge_connector_permission_grant", + "tableTo": "knowledge_connector_permission_snapshot", + "columnsFrom": ["connector_id"], + "columnsTo": ["connector_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcpg_pk": { + "name": "kcpg_pk", + "columns": ["connector_id", "group_key", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcpg_group_check": { + "name": "kcpg_group_check", + "value": "length(\"knowledge_connector_permission_grant\".\"group_key\") BETWEEN 1 AND 255" + }, + "kcpg_subject_check": { + "name": "kcpg_subject_check", + "value": "\"knowledge_connector_permission_grant\".\"subject_token\" ~ '^u:[^[:space:]A-Z]+@[^[:space:]A-Z]+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_snapshot": { + "name": "knowledge_connector_permission_snapshot", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "kcps_connector_fk": { + "name": "kcps_connector_fk", + "tableFrom": "knowledge_connector_permission_snapshot", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcps_revision_check": { + "name": "kcps_revision_check", + "value": "\"knowledge_connector_permission_snapshot\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_skipped": { + "name": "docs_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "listed_count": { + "name": "listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_failure_class": { + "name": "database_failure_class", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_terminal_started_at_idx": { + "name": "kcsl_terminal_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" IN ('completed', 'failed')", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcsl_database_failure_class_check": { + "name": "kcsl_database_failure_class_check", + "value": "\"knowledge_connector_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_document_observation": { + "name": "knowledge_document_observation", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kdo_member_idx": { + "name": "kdo_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_document_observation_document_id_document_id_fk": { + "name": "knowledge_document_observation_document_id_document_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": { + "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "knowledge_connector_member", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_document_observation_document_id_member_id_pk": { + "name": "knowledge_document_observation_document_id_member_id_pk", + "columns": ["document_id", "member_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_external_directory": { + "name": "knowledge_external_directory", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_sync_at": { + "name": "last_complete_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ked_organization_id_idx": { + "name": "ked_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_workspace_identity_unique": { + "name": "ked_workspace_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_organization_identity_unique": { + "name": "ked_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_directory_workspace_id_workspace_id_fk": { + "name": "knowledge_external_directory_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_external_directory_organization_id_organization_id_fk": { + "name": "knowledge_external_directory_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ked_owner_check": { + "name": "ked_owner_check", + "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group": { + "name": "knowledge_external_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_group_id": { + "name": "external_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keg_organization_id_idx": { + "name": "keg_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_identity_unique": { + "name": "keg_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_synced_idx": { + "name": "keg_organization_synced_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_identity_unique": { + "name": "keg_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_workspace_synced_idx": { + "name": "keg_workspace_synced_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_group_organization_id_organization_id_fk": { + "name": "knowledge_external_group_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "keg_workspace_fk": { + "name": "keg_workspace_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "keg_owner_check": { + "name": "keg_owner_check", + "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group_member": { + "name": "knowledge_external_group_member", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kegm_subject_token_idx": { + "name": "kegm_subject_token_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kegm_group_fk": { + "name": "kegm_group_fk", + "tableFrom": "knowledge_external_group_member", + "tableTo": "knowledge_external_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_external_group_member_group_id_subject_token_pk": { + "name": "knowledge_external_group_member_group_id_subject_token_pk", + "columns": ["group_id", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_projection_dirty": { + "name": "knowledge_projection_dirty", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "content": { + "name": "content", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marked_at": { + "name": "marked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "knowledge_projection_dirty_marked_at_idx": { + "name": "knowledge_projection_dirty_marked_at_idx", + "columns": [ + { + "expression": "marked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_projection_dirty_document_id_document_id_fk": { + "name": "knowledge_projection_dirty_document_id_document_id_fk", + "tableFrom": "knowledge_projection_dirty", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_organization_id_organization_id_fk": { + "name": "mcp_server_oauth_organization_id_organization_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_server_oauth_owner_check": { + "name": "mcp_server_oauth_owner_check", + "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_connector_id": { + "name": "managed_connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config_version": { + "name": "oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_organization_id_idx": { + "name": "mcp_servers_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_idx": { + "name": "mcp_servers_credential_group_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_managed_connector_unique": { + "name": "mcp_servers_credential_group_managed_connector_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "managed_connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_organization_id_organization_id_fk": { + "name": "mcp_servers_organization_id_organization_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_credential_group_id_credential_group_id_fk": { + "name": "mcp_servers_credential_group_id_credential_group_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_servers_owner_check": { + "name": "mcp_servers_owner_check", + "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1" + }, + "mcp_servers_organization_managed_check": { + "name": "mcp_servers_organization_managed_check", + "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL" + }, + "mcp_servers_credential_group_managed_connector_check": { + "name": "mcp_servers_credential_group_managed_connector_check", + "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL" + }, + "mcp_servers_managed_connector_oauth_check": { + "name": "mcp_servers_managed_connector_oauth_check", + "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'" + } + }, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "storage_version": { + "name": "storage_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "encrypted_context_summary": { + "name": "encrypted_context_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_artifact": { + "name": "memory_artifact", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "memory_artifact_key_idx": { + "name": "memory_artifact_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_artifact_memory_id_memory_id_fk": { + "name": "memory_artifact_memory_id_memory_id_fk", + "tableFrom": "memory_artifact", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_artifact_key_execution_large_values_key_fk": { + "name": "memory_artifact_key_execution_large_values_key_fk", + "tableFrom": "memory_artifact", + "tableTo": "execution_large_values", + "columnsFrom": ["key"], + "columnsTo": ["key"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memory_artifact_memory_id_key_pk": { + "name": "memory_artifact_memory_id_key_pk", + "columns": ["memory_id", "key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_item": { + "name": "memory_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "memory_item_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "append_key": { + "name": "append_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_status": { + "name": "provenance_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_entries": { + "name": "provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memory_item_append_unique": { + "name": "memory_item_append_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "append_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_item_sequence_idx": { + "name": "memory_item_sequence_idx", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_item_memory_id_memory_id_fk": { + "name": "memory_item_memory_id_memory_id_fk", + "tableFrom": "memory_item", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_item_kind_check": { + "name": "memory_item_kind_check", + "value": "\"memory_item\".\"kind\" IN ('message', 'exchange')" + }, + "memory_item_provenance_status_check": { + "name": "memory_item_provenance_status_check", + "value": "\"memory_item\".\"provenance_status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_benchmark_runs": { + "name": "mothership_benchmark_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "benchmark_id": { + "name": "benchmark_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "evaluation_key": { + "name": "evaluation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "correct": { + "name": "correct", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "automatic_correct": { + "name": "automatic_correct", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total": { + "name": "total", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_benchmark_runs_benchmark_created_idx": { + "name": "mothership_benchmark_runs_benchmark_created_idx", + "columns": [ + { + "expression": "benchmark_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_benchmark_runs_benchmark_id_mothership_benchmarks_id_fk": { + "name": "mothership_benchmark_runs_benchmark_id_mothership_benchmarks_id_fk", + "tableFrom": "mothership_benchmark_runs", + "tableTo": "mothership_benchmarks", + "columnsFrom": ["benchmark_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mothership_benchmark_runs_score_check": { + "name": "mothership_benchmark_runs_score_check", + "value": "\"mothership_benchmark_runs\".\"total\" > 0 AND \"mothership_benchmark_runs\".\"correct\" >= 0 AND \"mothership_benchmark_runs\".\"correct\" <= \"mothership_benchmark_runs\".\"total\"" + }, + "mothership_benchmark_runs_version_check": { + "name": "mothership_benchmark_runs_version_check", + "value": "\"mothership_benchmark_runs\".\"version\" > 0" + }, + "mothership_benchmark_runs_automatic_score_check": { + "name": "mothership_benchmark_runs_automatic_score_check", + "value": "\"mothership_benchmark_runs\".\"automatic_correct\" >= 0 AND \"mothership_benchmark_runs\".\"automatic_correct\" <= \"mothership_benchmark_runs\".\"total\"" + } + }, + "isRLSEnabled": false + }, + "public.mothership_benchmarks": { + "name": "mothership_benchmarks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_as_user_id": { + "name": "run_as_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "running_stage": { + "name": "running_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "planner_chat_id": { + "name": "planner_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_benchmarks_owner_created_idx": { + "name": "mothership_benchmarks_owner_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mothership_benchmarks_workspace_idx": { + "name": "mothership_benchmarks_workspace_idx", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_benchmarks_organization_id_organization_id_fk": { + "name": "mothership_benchmarks_organization_id_organization_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_user_id_user_id_fk": { + "name": "mothership_benchmarks_user_id_user_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_run_as_user_id_user_id_fk": { + "name": "mothership_benchmarks_run_as_user_id_user_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "user", + "columnsFrom": ["run_as_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_source_workspace_id_workspace_id_fk": { + "name": "mothership_benchmarks_source_workspace_id_workspace_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mothership_benchmarks_version_check": { + "name": "mothership_benchmarks_version_check", + "value": "\"mothership_benchmarks\".\"version\" > 0" + }, + "mothership_benchmarks_attempt_check": { + "name": "mothership_benchmarks_attempt_check", + "value": "(\"mothership_benchmarks\".\"running_stage\" IS NULL AND \"mothership_benchmarks\".\"attempt_id\" IS NULL AND \"mothership_benchmarks\".\"lease_expires_at\" IS NULL) OR (\"mothership_benchmarks\".\"running_stage\" IS NOT NULL AND \"mothership_benchmarks\".\"running_stage\" IN ('distill', 'redact', 'plan', 'reconstruct', 'grade') AND \"mothership_benchmarks\".\"attempt_id\" IS NOT NULL AND \"mothership_benchmarks\".\"lease_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_memory_selections": { + "name": "mothership_memory_selections", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_memory_selections_user_id_user_id_fk": { + "name": "mothership_memory_selections_user_id_user_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_selections_organization_id_organization_id_fk": { + "name": "mothership_memory_selections_organization_id_organization_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_selections_space_id_mothership_memory_spaces_id_fk": { + "name": "mothership_memory_selections_space_id_mothership_memory_spaces_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "mothership_memory_spaces", + "columnsFrom": ["space_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mothership_memory_selections_user_id_organization_id_pk": { + "name": "mothership_memory_selections_user_id_organization_id_pk", + "columns": ["user_id", "organization_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_memory_spaces": { + "name": "mothership_memory_spaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_memory_spaces_owner_idx": { + "name": "mothership_memory_spaces_owner_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_memory_spaces_user_id_user_id_fk": { + "name": "mothership_memory_spaces_user_id_user_id_fk", + "tableFrom": "mothership_memory_spaces", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_spaces_organization_id_organization_id_fk": { + "name": "mothership_memory_spaces_organization_id_organization_id_fk", + "tableFrom": "mothership_memory_spaces", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_resource_effects": { + "name": "mothership_resource_effects", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effect_id": { + "name": "effect_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_resource_effects_chat_id_copilot_chats_id_fk": { + "name": "mothership_resource_effects_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_resource_effects", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mothership_resource_effects_chat_id_effect_id_pk": { + "name": "mothership_resource_effects_chat_id_effect_id_pk", + "columns": ["chat_id", "effect_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_session_id_idx": { + "name": "oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_id_idx": { + "name": "oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_user_client_idx": { + "name": "oauth_access_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": ["refresh_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_access_token_search_resource_check": { + "name": "oauth_access_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_access_token\".\"scopes\")) OR \"oauth_access_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_client_user_id_idx": { + "name": "oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_consent_user_client_reference_unique": { + "name": "oauth_consent_user_client_reference_unique", + "nullsNotDistinct": true, + "columns": ["user_id", "client_id", "reference_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_id": { + "name": "family_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_refresh_token_client_id_idx": { + "name": "oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_session_id_idx": { + "name": "oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_user_client_idx": { + "name": "oauth_refresh_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_expires_at_idx": { + "name": "oauth_refresh_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_family_id_oauth_token_family_id_fk": { + "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_token_family", + "columnsFrom": ["family_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + }, + "oauth_refresh_token_family_generation_unique": { + "name": "oauth_refresh_token_family_generation_unique", + "nullsNotDistinct": false, + "columns": ["family_id", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_refresh_token_generation_check": { + "name": "oauth_refresh_token_generation_check", + "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000" + }, + "oauth_refresh_token_search_resource_check": { + "name": "oauth_refresh_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_refresh_token\".\"scopes\")) OR \"oauth_refresh_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_token_family": { + "name": "oauth_token_family", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_generation": { + "name": "current_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_token_family_client_id_idx": { + "name": "oauth_token_family_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_session_id_idx": { + "name": "oauth_token_family_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_user_client_idx": { + "name": "oauth_token_family_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_consent_id_idx": { + "name": "oauth_token_family_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_expires_at_idx": { + "name": "oauth_token_family_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_token_family_client_id_oauth_client_client_id_fk": { + "name": "oauth_token_family_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_session_id_session_id_fk": { + "name": "oauth_token_family_session_id_session_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_token_family_user_id_user_id_fk": { + "name": "oauth_token_family_user_id_user_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_consent_id_oauth_consent_id_fk": { + "name": "oauth_token_family_consent_id_oauth_consent_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_consent", + "columnsFrom": ["consent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "oauth_token_family_generation_check": { + "name": "oauth_token_family_generation_check", + "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000" + } + }, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "require_sso": { + "name": "require_sso", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_access_request_settings": { + "name": "organization_access_request_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "allow_requests": { + "name": "allow_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_access_request_settings_organization_id_organization_id_fk": { + "name": "organization_access_request_settings_organization_id_organization_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_access_request_settings_updated_by_user_id_fk": { + "name": "organization_access_request_settings_updated_by_user_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_byok_keys": { + "name": "organization_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_byok_organization_provider_idx": { + "name": "organization_byok_organization_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_byok_keys_organization_id_organization_id_fk": { + "name": "organization_byok_keys_organization_id_organization_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_byok_keys_created_by_user_id_fk": { + "name": "organization_byok_keys_created_by_user_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_history": { + "name": "organization_search_history", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "queries": { + "name": "queries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "organization_search_history_user_idx": { + "name": "organization_search_history_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_search_history_organization_id_organization_id_fk": { + "name": "organization_search_history_organization_id_organization_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_search_history_user_id_user_id_fk": { + "name": "organization_search_history_user_id_user_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_history_organization_id_user_id_pk": { + "name": "organization_search_history_organization_id_user_id_pk", + "columns": ["organization_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_integration": { + "name": "organization_search_integration", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_search_integration_organization_id_organization_id_fk": { + "name": "organization_search_integration_organization_id_organization_id_fk", + "tableFrom": "organization_search_integration", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_integration_organization_id_connector_type_pk": { + "name": "organization_search_integration_organization_id_connector_type_pk", + "columns": ["organization_id", "connector_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_mcp_invocation": { + "name": "organization_search_mcp_invocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_search_mcp_invocation_org_created_idx": { + "name": "organization_search_mcp_invocation_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_search_mcp_invocation_user_idx": { + "name": "organization_search_mcp_invocation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_search_mcp_invocation_org_fk": { + "name": "org_search_mcp_invocation_org_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_search_mcp_invocation_user_fk": { + "name": "org_search_mcp_invocation_user_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_search_mcp_invocation_tool_check": { + "name": "organization_search_mcp_invocation_tool_check", + "value": "\"organization_search_mcp_invocation\".\"tool_name\" IN ('search', 'read_document', 'chat')" + }, + "organization_search_mcp_invocation_outcome_check": { + "name": "organization_search_mcp_invocation_outcome_check", + "value": "\"organization_search_mcp_invocation\".\"outcome\" IN ('success', 'error', 'cancelled', 'rate_limited')" + }, + "organization_search_mcp_invocation_duration_check": { + "name": "organization_search_mcp_invocation_duration_check", + "value": "\"organization_search_mcp_invocation\".\"duration_ms\" >= 0" + }, + "organization_search_mcp_invocation_client_name_check": { + "name": "organization_search_mcp_invocation_client_name_check", + "value": "length(\"organization_search_mcp_invocation\".\"client_name\") <= 256" + }, + "organization_search_mcp_invocation_auth_check": { + "name": "organization_search_mcp_invocation_auth_check", + "value": "(\"organization_search_mcp_invocation\".\"auth_kind\" = 'oauth_access_token' AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NOT NULL)\n OR (\"organization_search_mcp_invocation\".\"auth_kind\" IN ('personal_api_key', 'workspace_api_key') AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NULL AND \"organization_search_mcp_invocation\".\"client_name\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.organization_secret": { + "name": "organization_secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_shared_unique": { + "name": "organization_secret_shared_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_member_unique": { + "name": "organization_secret_member_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_owner_idx": { + "name": "organization_secret_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_id_organization_secret_source_id_fk": { + "name": "organization_secret_source_id_organization_secret_source_id_fk", + "tableFrom": "organization_secret", + "tableTo": "organization_secret_source", + "columnsFrom": ["source_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_secret_owner_user_id_user_id_fk": { + "name": "organization_secret_owner_user_id_user_id_fk", + "tableFrom": "organization_secret", + "tableTo": "user", + "columnsFrom": ["owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_secret_source": { + "name": "organization_secret_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_source_org_unique": { + "name": "organization_secret_source_org_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_organization_id_organization_id_fk": { + "name": "organization_secret_source_organization_id_organization_id_fk", + "tableFrom": "organization_secret_source", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_secret_source_mode_check": { + "name": "organization_secret_source_mode_check", + "value": "\"organization_secret_source\".\"mode\" IN ('organization', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_pending_type_available_idx": { + "name": "outbox_event_pending_type_available_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_event\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_organization_id_idx": { + "name": "pending_draft_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_org": { + "name": "pending_draft_user_provider_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_organization_id_organization_id_fk": { + "name": "pending_credential_draft_organization_id_organization_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_draft_owner_check": { + "name": "pending_draft_owner_check", + "value": "num_nonnulls(\"pending_credential_draft\".\"workspace_id\", \"pending_credential_draft\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.permission_access_request": { + "name": "permission_access_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_id": { + "name": "requester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "target_label": { + "name": "target_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decision_reason": { + "name": "decision_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by": { + "name": "decided_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "permission_access_request_pending_unique": { + "name": "permission_access_request_pending_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"permission_access_request\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_org_queue_idx": { + "name": "permission_access_request_org_queue_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_requester_idx": { + "name": "permission_access_request_requester_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_access_request_organization_id_organization_id_fk": { + "name": "permission_access_request_organization_id_organization_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_requester_id_user_id_fk": { + "name": "permission_access_request_requester_id_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["requester_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_decided_by_user_id_fk": { + "name": "permission_access_request_decided_by_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["decided_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "permission_access_request_status_check": { + "name": "permission_access_request_status_check", + "value": "\"permission_access_request\".\"status\" in ('pending', 'fulfilled', 'declined', 'cancelled', 'closed')" + } + }, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "membership_mode": { + "name": "membership_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_scope_key": { + "name": "organization_scope_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END", + "type": "stored" + } + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_organization_archive_id_idx": { + "name": "project_organization_archive_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_archive_id_idx": { + "name": "project_owner_archive_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_organization_id_organization_id_fk": { + "name": "project_organization_id_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "project_owner_id_user_id_fk": { + "name": "project_owner_id_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_id_organization_scope_unique": { + "name": "project_id_organization_scope_unique", + "nullsNotDistinct": false, + "columns": ["id", "organization_scope_key"] + } + }, + "policies": {}, + "checkConstraints": { + "project_name_length": { + "name": "project_name_length", + "value": "char_length(btrim(\"project\".\"name\")) BETWEEN 1 AND 100" + } + }, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_entity_id_idx": { + "name": "public_share_entity_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "public_share_entity_binding_check": { + "name": "public_share_entity_binding_check", + "value": "\n (\"public_share\".\"entity_type\" IS NULL AND \"public_share\".\"entity_id\" IS NULL AND \"public_share\".\"workspace_id\" IS NOT NULL)\n OR (\"public_share\".\"entity_type\" IS NOT NULL AND \"public_share\".\"entity_id\" IS NOT NULL AND char_length(\"public_share\".\"entity_id\") > 0\n AND ((\"public_share\".\"entity_type\" = 'workspace' AND \"public_share\".\"workspace_id\" IS NOT NULL AND \"public_share\".\"entity_id\" = \"public_share\".\"workspace_id\")\n OR (\"public_share\".\"entity_type\" = 'project' AND \"public_share\".\"workspace_id\" IS NULL AND \"public_share\".\"resource_type\" = 'file')))\n " + } + }, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_state": { + "name": "capacity_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_policy": { + "name": "resource_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "resource_policy_organization_id_idx": { + "name": "resource_policy_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_resource_unique": { + "name": "resource_policy_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_workspace_id_idx": { + "name": "resource_policy_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_policy_workspace_id_workspace_id_fk": { + "name": "resource_policy_workspace_id_workspace_id_fk", + "tableFrom": "resource_policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_organization_id_organization_id_fk": { + "name": "resource_policy_organization_id_organization_id_fk", + "tableFrom": "resource_policy", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_created_by_user_id_fk": { + "name": "resource_policy_created_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_policy_updated_by_user_id_fk": { + "name": "resource_policy_updated_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "resource_policy_owner_check": { + "name": "resource_policy_owner_check", + "value": "num_nonnulls(\"resource_policy\".\"workspace_id\", \"resource_policy\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection": { + "name": "scim_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconcile_lock_token": { + "name": "reconcile_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconcile_lease_at": { + "name": "reconcile_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciled_at": { + "name": "reconciled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_connection_organization_unique": { + "name": "scim_connection_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_connection_reconcile_due_idx": { + "name": "scim_connection_reconcile_due_idx", + "columns": [ + { + "expression": "reconciled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_connection_organization_id_organization_id_fk": { + "name": "scim_connection_organization_id_organization_id_fk", + "tableFrom": "scim_connection", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_connection_created_by_user_id_fk": { + "name": "scim_connection_created_by_user_id_fk", + "tableFrom": "scim_connection", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_credential": { + "name": "scim_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_credential_token_hash_unique": { + "name": "scim_credential_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_credential_connection_idx": { + "name": "scim_credential_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_credential_connection_id_scim_connection_id_fk": { + "name": "scim_credential_connection_id_scim_connection_id_fk", + "tableFrom": "scim_credential", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_credential_revoked_by_user_id_fk": { + "name": "scim_credential_revoked_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scim_credential_created_by_user_id_fk": { + "name": "scim_credential_created_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_connection_display_name_unique": { + "name": "scim_group_connection_display_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_external_id_unique": { + "name": "scim_group_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_order_idx": { + "name": "scim_group_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_connection_id_scim_connection_id_fk": { + "name": "scim_group_connection_id_scim_connection_id_fk", + "tableFrom": "scim_group", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_mapping": { + "name": "scim_group_mapping", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_mapping_group_idx": { + "name": "scim_group_mapping_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_permission_group_idx": { + "name": "scim_group_mapping_permission_group_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_workspace_idx": { + "name": "scim_group_mapping_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_group_target_unique": { + "name": "scim_group_mapping_group_target_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"permission_group_id\", \"workspace_id\", \"role\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_mapping_group_id_scim_group_id_fk": { + "name": "scim_group_mapping_group_id_scim_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_permission_group_id_permission_group_id_fk": { + "name": "scim_group_mapping_permission_group_id_permission_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_workspace_id_workspace_id_fk": { + "name": "scim_group_mapping_workspace_id_workspace_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_created_by_user_id_fk": { + "name": "scim_group_mapping_created_by_user_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scim_group_mapping_target_shape": { + "name": "scim_group_mapping_target_shape", + "value": "(\n (\"scim_group_mapping\".\"target_kind\" = 'permission_group' AND \"scim_group_mapping\".\"permission_group_id\" IS NOT NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'workspace' AND \"scim_group_mapping\".\"workspace_id\" IS NOT NULL AND \"scim_group_mapping\".\"permission_type\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'org_role' AND \"scim_group_mapping\".\"role\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_member_group_user_unique": { + "name": "scim_group_member_group_user_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_member_scim_user_idx": { + "name": "scim_group_member_scim_user_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "baseline_permission": { + "name": "baseline_permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'directory'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_projection_grant_user_target_unique": { + "name": "scim_projection_grant_user_target_unique", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grant_connection_idx": { + "name": "scim_projection_grant_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_connection_id_scim_connection_id_fk": { + "name": "scim_projection_grant_connection_id_scim_connection_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_request_log": { + "name": "scim_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scim_type": { + "name": "scim_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_request_log_connection_created_idx": { + "name": "scim_request_log_connection_created_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_request_log_connection_id_scim_connection_id_fk": { + "name": "scim_request_log_connection_id_scim_connection_id_fk", + "tableFrom": "scim_request_log", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_connection_user_unique": { + "name": "scim_user_connection_user_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_user_name_unique": { + "name": "scim_user_connection_user_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_external_id_unique": { + "name": "scim_user_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_order_idx": { + "name": "scim_user_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_user_idx": { + "name": "scim_user_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_connection_id_scim_connection_id_fk": { + "name": "scim_user_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user_tombstone": { + "name": "scim_user_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_tombstone_connection_external_id_unique": { + "name": "scim_user_tombstone_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_tombstone_user_idx": { + "name": "scim_user_tombstone_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_tombstone_connection_id_scim_connection_id_fk": { + "name": "scim_user_tombstone_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_tombstone_user_id_user_id_fk": { + "name": "scim_user_tombstone_user_id_user_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_usage": { + "name": "secret_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_name": { + "name": "secret_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_scope": { + "name": "secret_scope", + "type": "secret_usage_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret_owner_user_id": { + "name": "secret_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source": { + "name": "source", + "type": "secret_usage_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_execution_id": { + "name": "last_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_trigger": { + "name": "last_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_usage_bucket_unique": { + "name": "secret_usage_bucket_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_usage_secret_recent_idx": { + "name": "secret_usage_secret_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_usage_workspace_id_workspace_id_fk": { + "name": "secret_usage_workspace_id_workspace_id_fk", + "tableFrom": "secret_usage", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_focus_on_click": { + "name": "auto_focus_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_app": { + "name": "slack_app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_client_secret": { + "name": "encrypted_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_signing_secret": { + "name": "encrypted_signing_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "slack_app_organization_id_organization_id_fk": { + "name": "slack_app_organization_id_organization_id_fk", + "tableFrom": "slack_app", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_app_owner_check": { + "name": "slack_app_owner_check", + "value": "(\"slack_app\".\"kind\" = 'custom' AND \"slack_app\".\"organization_id\" IS NOT NULL) OR (\"slack_app\".\"kind\" = 'shared' AND \"slack_app\".\"organization_id\" IS NULL)" + }, + "slack_app_custom_credentials_check": { + "name": "slack_app_custom_credentials_check", + "value": "\"slack_app\".\"kind\" = 'shared' OR (\"slack_app\".\"client_id\" IS NOT NULL AND \"slack_app\".\"encrypted_client_secret\" IS NOT NULL AND \"slack_app\".\"encrypted_signing_secret\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.slack_search_installation": { + "name": "slack_search_installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "credential_version": { + "name": "credential_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_outcome": { + "name": "last_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_installation_organization_idx": { + "name": "slack_search_installation_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_credential_unique": { + "name": "slack_search_installation_credential_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_app_team_unique": { + "name": "slack_search_installation_app_team_unique", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_active_team_unique": { + "name": "slack_search_installation_active_team_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_installation\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_installation_organization_id_organization_id_fk": { + "name": "slack_search_installation_organization_id_organization_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_credential_id_credential_id_fk": { + "name": "slack_search_installation_credential_id_credential_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_slack_app_id_slack_app_id_fk": { + "name": "slack_search_installation_slack_app_id_slack_app_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_search_turn": { + "name": "slack_search_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "slack_search_turn_ordinal_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_key": { + "name": "conversation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "lease_id": { + "name": "lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_turn_event_unique": { + "name": "slack_search_turn_event_unique", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_pending_idx": { + "name": "slack_search_turn_pending_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_thread_idx": { + "name": "slack_search_turn_thread_idx", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_active_thread_unique": { + "name": "slack_search_turn_active_thread_unique", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_turn\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_turn_installation_id_slack_search_installation_id_fk": { + "name": "slack_search_turn_installation_id_slack_search_installation_id_fk", + "tableFrom": "slack_search_turn", + "tableTo": "slack_search_installation", + "columnsFrom": ["installation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "primary_provider_id": { + "name": "primary_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "jit_provisioning_enabled": { + "name": "jit_provisioning_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "last_closed_period_start": { + "name": "last_closed_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_cycle_close_lagging_idx": { + "name": "subscription_cycle_close_lagging_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_capability_governed_user_id_user_id_fk": { + "name": "table_row_executions_capability_governed_user_id_user_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_governed_active_idx": { + "name": "table_run_dispatches_governed_active_idx", + "columns": [ + { + "expression": "capability_governed_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_run_dispatches\".\"status\" IN ('pending', 'dispatching')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "table_run_dispatches_capability_governed_user_id_user_id_fk": { + "name": "table_run_dispatches_capability_governed_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_workspace_created_idx": { + "name": "table_views_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspension_source": { + "name": "suspension_source", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_lower_idx": { + "name": "user_email_lower_idx", + "columns": [ + { + "expression": "lower(btrim(\"email\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_created_by_idx": { + "name": "user_table_def_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_changes": { + "name": "user_table_row_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_table_row_changes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_delta": { + "name": "row_delta", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_table_row_changes_table_id_idx": { + "name": "user_table_row_changes_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_row_changes_table_id_user_table_definitions_id_fk": { + "name": "user_table_row_changes_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_row_changes", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_created_by_idx": { + "name": "user_table_rows_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_rows\".\"created_by\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": { + "fastupdate": "off" + } + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_created_id_idx": { + "name": "user_table_rows_table_created_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_active_workspace_sort_idx": { + "name": "workflow_active_workspace_sort_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_activity_level_idx": { + "name": "workflow_execution_logs_workspace_activity_level_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "total_duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_started_at_idx": { + "name": "workflow_execution_logs_redacting_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'redacting'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_deadline_idx": { + "name": "workflow_execution_logs_redacting_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_test": { + "name": "workflow_test", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_file_id": { + "name": "body_file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cases": { + "name": "cases", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_test_workspace_name_unique": { + "name": "workflow_test_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_test\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_test_body_file_unique": { + "name": "workflow_test_body_file_unique", + "columns": [ + { + "expression": "body_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_test_workspace_id_workspace_id_fk": { + "name": "workflow_test_workspace_id_workspace_id_fk", + "tableFrom": "workflow_test", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_body_file_id_workspace_files_id_fk": { + "name": "workflow_test_body_file_id_workspace_files_id_fk", + "tableFrom": "workflow_test", + "tableTo": "workspace_files", + "columnsFrom": ["body_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_test_created_by_user_id_user_id_fk": { + "name": "workflow_test_created_by_user_id_user_id_fk", + "tableFrom": "workflow_test", + "tableTo": "user", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_test_run": { + "name": "workflow_test_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "test_id": { + "name": "test_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "passed": { + "name": "passed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped": { + "name": "skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ran_against": { + "name": "ran_against", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "triggered_by_actor": { + "name": "triggered_by_actor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_test_run_test_version_started_idx": { + "name": "workflow_test_run_test_version_started_idx", + "columns": [ + { + "expression": "test_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_test_run_test_id_workflow_test_id_fk": { + "name": "workflow_test_run_test_id_workflow_test_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "workflow_test", + "columnsFrom": ["test_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_run_workspace_id_workspace_id_fk": { + "name": "workflow_test_run_workspace_id_workspace_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_run_triggered_by_user_id_user_id_fk": { + "name": "workflow_test_run_triggered_by_user_id_user_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_test_run_version_check": { + "name": "workflow_test_run_version_check", + "value": "\"workflow_test_run\".\"version\" IN ('draft', 'deployed')" + }, + "workflow_test_run_status_check": { + "name": "workflow_test_run_status_check", + "value": "\"workflow_test_run\".\"status\" IN ('running', 'passed', 'failed', 'error')" + }, + "workflow_test_run_completed_check": { + "name": "workflow_test_run_completed_check", + "value": "(\"workflow_test_run\".\"status\" = 'running') = (\"workflow_test_run\".\"completed_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_scope_key": { + "name": "organization_scope_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END", + "type": "stored" + } + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fork_sync_new_workflows_excluded": { + "name": "fork_sync_new_workflows_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_project_id_id_idx": { + "name": "workspace_project_id_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_project_id_project_id_fk": { + "name": "workspace_project_id_project_id_fk", + "tableFrom": "workspace", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_project_organization_fk": { + "name": "workspace_project_organization_fk", + "tableFrom": "workspace", + "tableTo": "project", + "columnsFrom": ["project_id", "organization_scope_key"], + "columnsTo": ["id", "organization_scope_key"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_fork_project_fk": { + "name": "workspace_fork_project_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id", "project_id"], + "columnsTo": ["id", "project_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_id_project_unique": { + "name": "workspace_id_project_unique", + "nullsNotDistinct": false, + "columns": ["id", "project_id"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_uploaded_by_idx": { + "name": "workspace_file_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_backfill": { + "name": "workspace_file_search_backfill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "after_workspace_id": { + "name": "after_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_entity_type": { + "name": "after_entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_entity_id": { + "name": "after_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_file_id": { + "name": "after_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_build": { + "name": "workspace_file_search_build", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_file_search_build_file_idx": { + "name": "workspace_file_search_build_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_build_cleanup_idx": { + "name": "workspace_file_search_build_cleanup_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_build\".\"expires_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_build_owner_check": { + "name": "file_search_build_owner_check", + "value": "(\"workspace_file_search_build\".\"entity_type\" IS NULL AND \"workspace_file_search_build\".\"entity_id\" IS NULL AND \"workspace_file_search_build\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_build\".\"entity_type\" = 'workspace' AND \"workspace_file_search_build\".\"entity_id\" = \"workspace_file_search_build\".\"workspace_id\") OR (\"workspace_file_search_build\".\"entity_type\" = 'project' AND length(\"workspace_file_search_build\".\"entity_id\") > 0 AND \"workspace_file_search_build\".\"workspace_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_chunk": { + "name": "workspace_file_search_chunk", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_start": { + "name": "line_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fragment": { + "name": "fragment", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "overlap": { + "name": "overlap", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "file_search_chunk_owner_content_idx": { + "name": "file_search_chunk_owner_content_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_chunk\".\"entity_type\" = 'project'", + "concurrently": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + }, + "workspace_file_search_chunk_line_idx": { + "name": "workspace_file_search_chunk_line_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "line_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_chunk_content_idx": { + "name": "workspace_file_search_chunk_content_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + } + }, + "foreignKeys": { + "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_chunk", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_chunk_pk": { + "name": "workspace_file_search_chunk_pk", + "columns": ["build_id", "ordinal"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_chunk_owner_check": { + "name": "file_search_chunk_owner_check", + "value": "(\"workspace_file_search_chunk\".\"entity_type\" IS NULL AND \"workspace_file_search_chunk\".\"entity_id\" IS NULL AND \"workspace_file_search_chunk\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_chunk\".\"entity_type\" = 'workspace' AND \"workspace_file_search_chunk\".\"entity_id\" = \"workspace_file_search_chunk\".\"workspace_id\") OR (\"workspace_file_search_chunk\".\"entity_type\" = 'project' AND length(\"workspace_file_search_chunk\".\"entity_id\") > 0 AND \"workspace_file_search_chunk\".\"workspace_id\" IS NULL)" + }, + "workspace_file_search_chunk_content_size": { + "name": "workspace_file_search_chunk_content_size", + "value": "octet_length(\"workspace_file_search_chunk\".\"content\") <= 8192" + }, + "workspace_file_search_chunk_position": { + "name": "workspace_file_search_chunk_position", + "value": "\"workspace_file_search_chunk\".\"ordinal\" >= 0 AND \"workspace_file_search_chunk\".\"line_start\" > 0 AND \"workspace_file_search_chunk\".\"overlap\" BETWEEN 0 AND 2" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_dispatch_queue": { + "name": "workspace_file_search_dispatch_queue", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_dispatch_queue_schedule_idx": { + "name": "workspace_file_search_dispatch_queue_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_queue_workspace_fk": { + "name": "workspace_file_search_queue_workspace_fk", + "tableFrom": "workspace_file_search_dispatch_queue", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_index": { + "name": "workspace_file_search_index", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "partial": { + "name": "partial", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_index_workspace_status_idx": { + "name": "workspace_file_search_index_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_pending_dispatch_idx": { + "name": "workspace_file_search_index_pending_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_active_dispatch_idx": { + "name": "workspace_file_search_index_active_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_index_file_fk": { + "name": "workspace_file_search_index_file_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_index_workspace_fk": { + "name": "workspace_file_search_index_workspace_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_index_pk": { + "name": "workspace_file_search_index_pk", + "columns": ["file_id", "source_content_updated_at"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_revision": { + "name": "workspace_file_search_revision", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "handoff_expires_at": { + "name": "handoff_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_search_revision_owner_pending_idx": { + "name": "file_search_revision_owner_pending_idx", + "columns": [ + { + "expression": "coalesce(\"entity_type\", 'workspace')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"entity_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "file_search_revision_owner_status_idx": { + "name": "file_search_revision_owner_status_idx", + "columns": [ + { + "expression": "coalesce(\"entity_type\", 'workspace')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"entity_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_workspace_status_idx": { + "name": "workspace_file_search_revision_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_build_idx": { + "name": "workspace_file_search_revision_build_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_pending_idx": { + "name": "workspace_file_search_revision_pending_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_active_idx": { + "name": "workspace_file_search_revision_active_idx", + "columns": [ + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_revision_file_id_workspace_files_id_fk": { + "name": "workspace_file_search_revision_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_revision_owner_check": { + "name": "file_search_revision_owner_check", + "value": "(\"workspace_file_search_revision\".\"entity_type\" IS NULL AND \"workspace_file_search_revision\".\"entity_id\" IS NULL AND \"workspace_file_search_revision\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_revision\".\"entity_type\" = 'workspace' AND \"workspace_file_search_revision\".\"entity_id\" = \"workspace_file_search_revision\".\"workspace_id\") OR (\"workspace_file_search_revision\".\"entity_type\" = 'project' AND length(\"workspace_file_search_revision\".\"entity_id\") > 0 AND \"workspace_file_search_revision\".\"workspace_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_segment": { + "name": "workspace_file_search_segment", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_number": { + "name": "segment_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_start": { + "name": "segment_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_length": { + "name": "line_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workspace_file_search_segment_workspace_revision_idx": { + "name": "workspace_file_search_segment_workspace_revision_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_segment_workspace_content_trgm_idx": { + "name": "workspace_file_search_segment_workspace_content_trgm_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_segment_file_fk": { + "name": "workspace_file_search_segment_file_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_segment_workspace_fk": { + "name": "workspace_file_search_segment_workspace_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_segment_pk": { + "name": "workspace_file_search_segment_pk", + "columns": ["file_id", "source_content_updated_at", "line_number", "segment_number"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_version": { + "name": "workspace_file_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "workspace_file_version_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "author_user_ids": { + "name": "author_user_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "restored_from_version": { + "name": "restored_from_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_status": { + "name": "secret_provenance_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_entries": { + "name": "secret_provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_version_file_version_unique": { + "name": "workspace_file_version_file_version_unique", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_key_unique": { + "name": "workspace_file_version_key_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_id_idx": { + "name": "workspace_file_version_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_superseded_idx": { + "name": "workspace_file_version_workspace_superseded_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "superseded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_version\".\"superseded_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_version_file_id_workspace_files_id_fk": { + "name": "workspace_file_version_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_version_workspace_id_workspace_id_fk": { + "name": "workspace_file_version_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_version_provenance_status_check": { + "name": "workspace_file_version_provenance_status_check", + "value": "\"workspace_file_version\".\"secret_provenance_status\" IS NULL OR \"workspace_file_version\".\"secret_provenance_status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "date_trunc('milliseconds', now())" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_project_id_idx": { + "name": "workspace_files_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_project_folder_name_active_unique": { + "name": "workspace_files_project_folder_name_active_unique", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"project_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_search_owner_keyset_idx": { + "name": "workspace_files_search_owner_keyset_idx", + "columns": [ + { + "expression": "(CASE WHEN \"project_id\" IS NOT NULL THEN 'project' ELSE 'workspace' END)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"project_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND ((\"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL) OR (\"workspace_files\".\"context\" = 'project' AND \"workspace_files\".\"project_id\" IS NOT NULL))", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_active_keyset_idx": { + "name": "workspace_files_workspace_active_keyset_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_display_name_idx": { + "name": "workspace_files_workspace_display_name_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "regexp_replace(regexp_replace(regexp_replace(normalize(\"original_name\", NFC), '^[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+|[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+$', '', 'g'), '[\\x01-\\x1f\\x7f]', '', 'g'), '[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+', ' ', 'g')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_organization_id_idx": { + "name": "workspace_files_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_project_id_project_id_fk": { + "name": "workspace_files_project_id_project_id_fk", + "tableFrom": "workspace_files", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_organization_id_organization_id_fk": { + "name": "workspace_files_organization_id_organization_id_fk", + "tableFrom": "workspace_files", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_files_owner_check": { + "name": "workspace_files_owner_check", + "value": "num_nonnulls(\"workspace_files\".\"workspace_id\", \"workspace_files\".\"project_id\", \"workspace_files\".\"organization_id\") <= 1" + }, + "workspace_files_workspace_binding_check": { + "name": "workspace_files_workspace_binding_check", + "value": "\"workspace_files\".\"context\" NOT IN ('workspace', 'chat', 'mothership', 'execution', 'workspace-logos') OR \"workspace_files\".\"workspace_id\" IS NOT NULL" + }, + "workspace_files_project_binding_check": { + "name": "workspace_files_project_binding_check", + "value": "(\"workspace_files\".\"project_id\" IS NOT NULL) = (\"workspace_files\".\"context\" = 'project') AND (\"workspace_files\".\"project_id\" IS NULL OR \"workspace_files\".\"chat_id\" IS NULL)" + }, + "workspace_files_organization_binding_check": { + "name": "workspace_files_organization_binding_check", + "value": "\"workspace_files\".\"organization_id\" IS NULL OR (\"workspace_files\".\"workspace_id\" IS NULL AND \"workspace_files\".\"context\" = 'knowledge-base' AND \"workspace_files\".\"folder_id\" IS NULL AND \"workspace_files\".\"chat_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_workflow_sync": { + "name": "workspace_fork_workflow_sync", + "schema": "", + "columns": { + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workflow_id": { + "name": "source_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_deployment_version_id": { + "name": "source_deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "workspace_fork_workflow_sync_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "promote_run_id": { + "name": "promote_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rollback_operation_id": { + "name": "rollback_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rolled_back_at": { + "name": "rolled_back_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_fork_workflow_sync_baseline_idx": { + "name": "workspace_fork_workflow_sync_baseline_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_fork_workflow_sync\".\"activated_at\" IS NOT NULL AND \"workspace_fork_workflow_sync\".\"rolled_back_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_run_idx": { + "name": "workspace_fork_workflow_sync_run_idx", + "columns": [ + { + "expression": "promote_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_rollback_idx": { + "name": "workspace_fork_workflow_sync_rollback_idx", + "columns": [ + { + "expression": "rollback_operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_fork_workflow_sync\".\"rollback_operation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_source_idx": { + "name": "workspace_fork_workflow_sync_source_idx", + "columns": [ + { + "expression": "source_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_target_idx": { + "name": "workspace_fork_workflow_sync_target_idx", + "columns": [ + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_child_workspace_idx": { + "name": "workspace_fork_workflow_sync_child_workspace_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_workflow_sync_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_workflow_sync_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_workflow_sync_source_workflow_id_workflow_id_fk": { + "name": "workspace_fork_workflow_sync_source_workflow_id_workflow_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workflow", + "columnsFrom": ["source_workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_workflow_sync_target_workflow_id_workflow_id_fk": { + "name": "workspace_fork_workflow_sync_target_workflow_id_workflow_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workflow", + "columnsFrom": ["target_workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operation_receipt": { + "name": "workspace_operation_receipt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operation_receipt_request_unique": { + "name": "workspace_operation_receipt_request_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operation_receipt_workspace_created_idx": { + "name": "workspace_operation_receipt_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operation_receipt_workspace_id_workspace_id_fk": { + "name": "workspace_operation_receipt_workspace_id_workspace_id_fk", + "tableFrom": "workspace_operation_receipt", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_visit": { + "name": "workspace_visit", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visited_at": { + "name": "visited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_visit_workspace_idx": { + "name": "workspace_visit_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_visit_user_id_user_id_fk": { + "name": "workspace_visit_user_id_user_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_visit_workspace_id_workspace_id_fk": { + "name": "workspace_visit_workspace_id_workspace_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_visit_user_id_workspace_id_pk": { + "name": "workspace_visit_user_id_workspace_id_pk", + "columns": ["user_id", "workspace_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_group_enrollment_status": { + "name": "credential_group_enrollment_status", + "schema": "public", + "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"] + }, + "public.credential_group_status": { + "name": "credential_group_status", + "schema": "public", + "values": ["active", "disabled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": [ + "oauth", + "managed_oauth", + "managed_mcp", + "env_workspace", + "env_personal", + "service_account", + "personal_token" + ] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.managed_oauth_credential_status": { + "name": "managed_oauth_credential_status", + "schema": "public", + "values": ["active", "needs_reauth", "revoked"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.secret_usage_scope": { + "name": "secret_usage_scope", + "schema": "public", + "values": ["workspace", "personal"] + }, + "public.secret_usage_source": { + "name": "secret_usage_source", + "schema": "public", + "values": ["workflow", "copilot", "mcp"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "project_file", + "table_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "organization_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool", "model_unbilled"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output", + "api-tool", + "workflow-test" + ] + }, + "public.workspace_file_search_index_status": { + "name": "workspace_file_search_index_status", + "schema": "public", + "values": ["pending", "ready", "skipped", "failed"] + }, + "public.workspace_file_version_source": { + "name": "workspace_file_version_source", + "schema": "public", + "values": ["upload", "user", "api", "copilot", "workflow", "collab", "revert", "unknown"] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "file_folder", + "mcp_server", + "workflow_mcp_server", + "custom_block", + "custom_tool", + "skill", + "sandbox" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": { + "public.copilot_async_tool_calls_persist_seq": { + "name": "copilot_async_tool_calls_persist_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/0432_snapshot.json b/packages/db/migrations/meta/0432_snapshot.json new file mode 100644 index 00000000000..71f1c159df6 --- /dev/null +++ b/packages/db/migrations/meta/0432_snapshot.json @@ -0,0 +1,31438 @@ +{ + "id": "cf98b964-a4e7-4850-ac16-a86e3a7b4006", + "prevId": "5c28fb1c-241a-4cdb-9061-480367c42f4a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_at": { + "name": "refresh_revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_code": { + "name": "refresh_revoked_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_token_hash": { + "name": "refresh_revoked_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_memory_turn": { + "name": "agent_memory_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_order": { + "name": "execution_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_memory_turn_invocation_unique": { + "name": "agent_memory_turn_invocation_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memory_turn_workflow_idx": { + "name": "agent_memory_turn_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memory_turn_memory_id_memory_id_fk": { + "name": "agent_memory_turn_memory_id_memory_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memory_turn_workflow_id_workflow_id_fk": { + "name": "agent_memory_turn_workflow_id_workflow_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_change": { + "name": "changelog_change", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "changelog_change_release_position_unique": { + "name": "changelog_change_release_position_unique", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changelog_change_release_id_changelog_release_id_fk": { + "name": "changelog_change_release_id_changelog_release_id_fk", + "tableFrom": "changelog_change", + "tableTo": "changelog_release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changelog_change_workflow_id_workflow_id_fk": { + "name": "changelog_change_workflow_id_workflow_id_fk", + "tableFrom": "changelog_change", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_change_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "changelog_change_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "changelog_change", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_change_chat_id_copilot_chats_id_fk": { + "name": "changelog_change_chat_id_copilot_chats_id_fk", + "tableFrom": "changelog_change", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_release": { + "name": "changelog_release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_major": { + "name": "version_major", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_minor": { + "name": "version_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_patch": { + "name": "version_patch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bump_reason": { + "name": "bump_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_file_id": { + "name": "body_file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "published_at": { + "name": "published_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "changelog_release_workspace_version_unique": { + "name": "changelog_release_workspace_version_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_major", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_minor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_patch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changelog_release_body_file_unique": { + "name": "changelog_release_body_file_unique", + "columns": [ + { + "expression": "body_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changelog_release_workspace_published_idx": { + "name": "changelog_release_workspace_published_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changelog_release_workspace_id_workspace_id_fk": { + "name": "changelog_release_workspace_id_workspace_id_fk", + "tableFrom": "changelog_release", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changelog_release_body_file_id_workspace_files_id_fk": { + "name": "changelog_release_body_file_id_workspace_files_id_fk", + "tableFrom": "changelog_release", + "tableTo": "workspace_files", + "columnsFrom": ["body_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "changelog_release_created_by_user_id_fk": { + "name": "changelog_release_created_by_user_id_fk", + "tableFrom": "changelog_release", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_release_updated_by_user_id_fk": { + "name": "changelog_release_updated_by_user_id_fk", + "tableFrom": "changelog_release", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "changelog_release_version_check": { + "name": "changelog_release_version_check", + "value": "\"changelog_release\".\"version_major\" >= 0 AND \"changelog_release\".\"version_minor\" >= 0 AND \"changelog_release\".\"version_patch\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_user_id_idx": { + "name": "chat_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_credential_token": { + "name": "client_credential_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_digest": { + "name": "access_token_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "client_credential_token_expires_at_idx": { + "name": "client_credential_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_credential_token\".\"access_token_digest\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_credential_token_access_token_digest_idx": { + "name": "client_credential_token_access_token_digest_idx", + "columns": [ + { + "expression": "access_token_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_credential_token\".\"access_token_digest\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_requested_at": { + "name": "permission_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pickup_deadline_at": { + "name": "pickup_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "persist_seq": { + "name": "persist_seq", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": "nextval('copilot_async_tool_calls_persist_seq')" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "browser_download_started_at": { + "name": "browser_download_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_started_at": { + "name": "execution_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_settled_at": { + "name": "execution_settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_owner_token": { + "name": "execution_owner_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_lease_expires_at": { + "name": "execution_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_revoked_at": { + "name": "execution_revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_attempts": { + "name": "execution_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "client_workflow_execution_id": { + "name": "client_workflow_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_processes": { + "name": "sandbox_processes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memory_space_id": { + "name": "memory_space_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_key": { + "name": "external_conversation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_metadata": { + "name": "external_conversation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_organization_id_idx": { + "name": "copilot_chats_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_external_conversation_unique": { + "name": "copilot_chats_external_conversation_unique", + "columns": [ + { + "expression": "external_conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"copilot_chats\".\"external_conversation_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_org_created_idx": { + "name": "copilot_chats_user_org_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_organization_id_organization_id_fk": { + "name": "copilot_chats_organization_id_organization_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "copilot_chats_owner_check": { + "name": "copilot_chats_owner_check", + "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1" + }, + "copilot_chats_organization_workflow_check": { + "name": "copilot_chats_organization_workflow_check", + "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "display_content": { + "name": "display_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_organization_request_stops": { + "name": "copilot_organization_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_organization_request_stops_user_id_user_id_fk": { + "name": "copilot_organization_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_organization_request_stops_organization_id_organization_id_fk": { + "name": "copilot_organization_request_stops_organization_id_organization_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_organization_request_stops_user_id_organization_id_stream_id_pk": { + "name": "copilot_organization_request_stops_user_id_organization_id_stream_id_pk", + "columns": ["user_id", "organization_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_request_stops": { + "name": "copilot_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_request_stops_user_id_user_id_fk": { + "name": "copilot_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_request_stops_workspace_id_workspace_id_fk": { + "name": "copilot_request_stops_workspace_id_workspace_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_request_stops_user_id_workspace_id_stream_id_pk": { + "name": "copilot_request_stops_user_id_workspace_id_stream_id_pk", + "columns": ["user_id", "workspace_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_execution_version": { + "name": "tool_execution_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_admission_closed_at": { + "name": "tool_admission_closed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "desktop_device_id": { + "name": "desktop_device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_started_at_idx": { + "name": "copilot_runs_chat_started_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_desktop_device_started_at_idx": { + "name": "copilot_runs_desktop_device_started_at_idx", + "columns": [ + { + "expression": "desktop_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_runs\".\"desktop_device_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_organization_id_organization_id_fk": { + "name": "copilot_runs_organization_id_organization_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_desktop_device_id_desktop_devices_id_fk": { + "name": "copilot_runs_desktop_device_id_desktop_devices_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "desktop_devices", + "columnsFrom": ["desktop_device_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_service_usage": { + "name": "copilot_service_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": false + }, + "worker_origin": { + "name": "worker_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_service_usage_pending_idx": { + "name": "copilot_service_usage_pending_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "delivered_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_personal_token": { + "name": "encrypted_personal_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_oauth_config_version": { + "name": "mcp_oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tools": { + "name": "mcp_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "mcp_tools_refreshed_at": { + "name": "mcp_tools_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_organization_id_idx": { + "name": "credential_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_organization_account_unique": { + "name": "credential_organization_account_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_org_personal_token_unique": { + "name": "credential_org_personal_token_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_mcp_server_idx": { + "name": "credential_mcp_server_idx", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_unique": { + "name": "credential_group_option_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_managed_mcp_enrollment_server_unique": { + "name": "credential_managed_mcp_enrollment_server_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_mcp'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_personal_token_identity_unique": { + "name": "credential_personal_token_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_organization_id_organization_id_fk": { + "name": "credential_organization_id_organization_id_fk", + "tableFrom": "credential", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_slack_app_id_slack_app_id_fk": { + "name": "credential_slack_app_id_slack_app_id_fk", + "tableFrom": "credential", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_mcp_server_id_mcp_servers_id_fk": { + "name": "credential_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "credential", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_owner_check": { + "name": "credential_owner_check", + "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1" + }, + "credential_organization_type_check": { + "name": "credential_organization_type_check", + "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')" + }, + "credential_personal_token_source_check": { + "name": "credential_personal_token_source_check", + "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_managed_mcp_source_check": { + "name": "credential_managed_mcp_source_check", + "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND provider_subject_id IS NULL\n AND provider_tenant_id IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_creator_source_check": { + "name": "credential_creator_source_check", + "value": "(type::text = 'managed_mcp') OR created_by IS NOT NULL" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_organization_id_idx": { + "name": "credential_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_organization_unique": { + "name": "credential_group_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_unique": { + "name": "credential_group_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_organization_id_organization_id_fk": { + "name": "credential_group_organization_id_organization_id_fk", + "tableFrom": "credential_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_owner_check": { + "name": "credential_group_owner_check", + "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_enrollment_group_user_unique": { + "name": "credential_group_enrollment_group_user_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_user_id_idx": { + "name": "credential_group_enrollment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_user_id_user_id_fk": { + "name": "credential_group_enrollment_user_id_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dashboard_workspace_id_unique": { + "name": "dashboard_workspace_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboard_created_by_user_id_fk": { + "name": "dashboard_created_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "dashboard_updated_by_user_id_fk": { + "name": "dashboard_updated_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.desktop_devices": { + "name": "desktop_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_version": { + "name": "app_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "desktop_devices_user_id_idx": { + "name": "desktop_devices_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "desktop_devices_session_id_idx": { + "name": "desktop_devices_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "desktop_devices_user_id_user_id_fk": { + "name": "desktop_devices_user_id_user_id_fk", + "tableFrom": "desktop_devices", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "desktop_devices_session_id_session_id_fk": { + "name": "desktop_devices_session_id_session_id_fk", + "tableFrom": "desktop_devices", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_queue_token": { + "name": "processing_queue_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_deferred_until": { + "name": "processing_deferred_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_recovery_after": { + "name": "processing_recovery_after", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{ws}'::text[]" + }, + "acl_requirements": { + "name": "acl_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "acl_verified_at": { + "name": "acl_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_modified_at": { + "name": "source_modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_seen_at": { + "name": "source_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_uploaded_by_idx": { + "name": "doc_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"uploaded_by\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_kb_source_modified_idx": { + "name": "doc_kb_source_modified_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_modified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_acl_gin_idx": { + "name": "doc_acl_gin_idx", + "columns": [ + { + "expression": "acl", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "array_ops" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_processing_recovery_idx": { + "name": "doc_connector_processing_recovery_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_connector_processing_status_idx": { + "name": "doc_connector_processing_status_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_source_lookup_idx": { + "name": "doc_connector_source_lookup_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_reconciliation_v2_idx": { + "name": "doc_connector_reconciliation_v2_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_tombstone_idx": { + "name": "doc_connector_tombstone_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NULL AND (\"document\".\"deleted_at\" IS NOT NULL OR \"document\".\"content_hash\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_live_idx": { + "name": "doc_connector_live_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag1_lower_idx": { + "name": "doc_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag2_lower_idx": { + "name": "doc_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag3_lower_idx": { + "name": "doc_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag4_lower_idx": { + "name": "doc_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag5_lower_idx": { + "name": "doc_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag6_lower_idx": { + "name": "doc_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag7_lower_idx": { + "name": "doc_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "doc_acl_token_shape_check": { + "name": "doc_acl_token_shape_check", + "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))" + } + }, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_384": { + "name": "embedding_384", + "type": "vector(384)", + "primaryKey": false, + "notNull": false + }, + "embedding_768": { + "name": "embedding_768", + "type": "vector(768)", + "primaryKey": false, + "notNull": false + }, + "embedding_1024": { + "name": "embedding_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "embedding_3072": { + "name": "embedding_3072", + "type": "vector(3072)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag1_lower_idx": { + "name": "emb_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag2_lower_idx": { + "name": "emb_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag3_lower_idx": { + "name": "emb_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag4_lower_idx": { + "name": "emb_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag5_lower_idx": { + "name": "emb_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag6_lower_idx": { + "name": "emb_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag7_lower_idx": { + "name": "emb_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": { + "fastupdate": "off" + } + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_width_check": { + "name": "embedding_width_check", + "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_search": { + "name": "embedding_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "binary": { + "name": "binary", + "type": "bit(1536)", + "primaryKey": false, + "notNull": false + }, + "binary_384": { + "name": "binary_384", + "type": "bit(384)", + "primaryKey": false, + "notNull": false + }, + "binary_768": { + "name": "binary_768", + "type": "bit(768)", + "primaryKey": false, + "notNull": false + }, + "binary_1024": { + "name": "binary_1024", + "type": "bit(1024)", + "primaryKey": false, + "notNull": false + }, + "binary_3072": { + "name": "binary_3072", + "type": "bit(3072)", + "primaryKey": false, + "notNull": false + }, + "vector": { + "name": "vector", + "type": "halfvec(1536)", + "primaryKey": false, + "notNull": false + }, + "vector_384": { + "name": "vector_384", + "type": "halfvec(384)", + "primaryKey": false, + "notNull": false + }, + "vector_512": { + "name": "vector_512", + "type": "halfvec(512)", + "primaryKey": false, + "notNull": false + }, + "vector_768": { + "name": "vector_768", + "type": "halfvec(768)", + "primaryKey": false, + "notNull": false + }, + "vector_1024": { + "name": "vector_1024", + "type": "halfvec(1024)", + "primaryKey": false, + "notNull": false + }, + "vector_3072": { + "name": "vector_3072", + "type": "halfvec(3072)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "embedding_search_kb_idx": { + "name": "embedding_search_kb_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_search_document_lookup_idx": { + "name": "embedding_search_document_lookup_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"embedding_search\".\"enabled\"", + "concurrently": true, + "method": "btree", + "with": {} + }, + "embedding_search_cosine_hnsw_idx": { + "name": "embedding_search_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_512_cosine_hnsw_idx": { + "name": "embedding_search_512_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_512", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_384_cosine_hnsw_idx": { + "name": "embedding_search_384_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_768_cosine_hnsw_idx": { + "name": "embedding_search_768_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_1024_cosine_hnsw_idx": { + "name": "embedding_search_1024_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_3072_cosine_hnsw_idx": { + "name": "embedding_search_3072_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_3072", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + } + }, + "foreignKeys": { + "embedding_search_id_embedding_id_fk": { + "name": "embedding_search_id_embedding_id_fk", + "tableFrom": "embedding_search", + "tableTo": "embedding", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_search_width_check": { + "name": "embedding_search_width_check", + "value": "num_nonnulls(\"binary\", \"binary_384\", \"binary_768\", \"binary_1024\", \"binary_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workspace_id_idx": { + "name": "execution_large_values_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_search_dependency": { + "name": "file_search_dependency", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "file_search_dependency_file_idx": { + "name": "file_search_dependency_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "file_search_dependency_owner_idx": { + "name": "file_search_dependency_owner_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_search_dependency_build_id_workspace_file_search_build_id_fk": { + "name": "file_search_dependency_build_id_workspace_file_search_build_id_fk", + "tableFrom": "file_search_dependency", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_search_dependency_build_id_file_id_pk": { + "name": "file_search_dependency_build_id_file_id_pk", + "columns": ["build_id", "file_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_search_dispatch_queue": { + "name": "file_search_dispatch_queue", + "schema": "", + "columns": { + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_search_dispatch_schedule_idx": { + "name": "file_search_dispatch_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "file_search_dispatch_queue_entity_type_entity_id_pk": { + "name": "file_search_dispatch_queue_entity_type_entity_id_pk", + "columns": ["entity_type", "entity_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_dispatch_owner_check": { + "name": "file_search_dispatch_owner_check", + "value": "\"file_search_dispatch_queue\".\"entity_type\" IN ('workspace', 'project') AND length(\"file_search_dispatch_queue\".\"entity_id\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_project_id_idx": { + "name": "folder_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "folder_project_resource_parent_name_active_unique": { + "name": "folder_project_resource_parent_name_active_unique", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL AND \"folder\".\"project_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_project_id_project_id_fk": { + "name": "folder_project_id_project_id_fk", + "tableFrom": "folder", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "folder_workspace_parent_owner_fk": { + "name": "folder_workspace_parent_owner_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id", "workspace_id", "resource_type"], + "columnsTo": ["id", "workspace_id", "resource_type"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folder_project_parent_owner_fk": { + "name": "folder_project_parent_owner_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id", "project_id", "resource_type"], + "columnsTo": ["id", "project_id", "resource_type"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "folder_workspace_owner_type_unique": { + "name": "folder_workspace_owner_type_unique", + "nullsNotDistinct": false, + "columns": ["id", "workspace_id", "resource_type"] + }, + "folder_project_owner_type_unique": { + "name": "folder_project_owner_type_unique", + "nullsNotDistinct": false, + "columns": ["id", "project_id", "resource_type"] + } + }, + "policies": {}, + "checkConstraints": { + "folder_owner_check": { + "name": "folder_owner_check", + "value": "num_nonnulls(\"folder\".\"workspace_id\", \"folder\".\"project_id\") = 1 AND (\"folder\".\"project_id\" IS NULL OR \"folder\".\"resource_type\" = 'file')" + } + }, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_search_index": { + "name": "is_search_index", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_organization_id_idx": { + "name": "kb_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_search_index_unique": { + "name": "kb_organization_search_index_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_name_active_unique": { + "name": "kb_organization_name_active_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_search_index_unique": { + "name": "kb_workspace_search_index_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_organization_id_organization_id_fk": { + "name": "knowledge_base_organization_id_organization_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kb_owner_check": { + "name": "kb_owner_check", + "value": "num_nonnulls(\"knowledge_base\".\"workspace_id\", \"knowledge_base\".\"organization_id\") = 1" + }, + "kb_organization_search_index_check": { + "name": "kb_organization_search_index_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"is_search_index\"" + }, + "kb_organization_folder_check": { + "name": "kb_organization_folder_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"folder_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "access_mode": { + "name": "access_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workspace'" + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_status": { + "name": "member_sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "member_sync_lock_token": { + "name": "member_sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_lock_lease_at": { + "name": "member_sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_member_sync_at": { + "name": "next_member_sync_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_at": { + "name": "last_member_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_error": { + "name": "last_member_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_consecutive_failures": { + "name": "member_sync_consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "access_rewrite_pending": { + "name": "access_rewrite_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image_removal_pending": { + "name": "image_removal_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "member_tombstone_cursor": { + "name": "member_tombstone_cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "member_resurrection_cursor": { + "name": "member_resurrection_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "directory_checkpoint": { + "name": "directory_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "next_directory_sync_at": { + "name": "next_directory_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detached_at": { + "name": "detached_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detach_reserved_bytes": { + "name": "detach_reserved_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_member_sync_due_idx": { + "name": "kc_member_sync_due_idx", + "columns": [ + { + "expression": "member_sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_member_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_directory_sync_due_idx": { + "name": "kc_directory_sync_due_idx", + "columns": [ + { + "expression": "next_directory_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_credential_group_id_credential_group_id_fk": { + "name": "knowledge_connector_credential_group_id_credential_group_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kc_access_mode_check": { + "name": "kc_access_mode_check", + "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')" + }, + "kc_member_sync_status_check": { + "name": "kc_member_sync_status_check", + "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')" + }, + "kc_sync_lock_exclusive_check": { + "name": "kc_sync_lock_exclusive_check", + "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member": { + "name": "knowledge_connector_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_listing_at": { + "name": "last_complete_listing_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_listed_count": { + "name": "last_listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_synced_through": { + "name": "member_synced_through", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewed_at": { + "name": "scope_renewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_cursor": { + "name": "scope_renewal_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_started_at": { + "name": "scope_renewal_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "change_cursor": { + "name": "change_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kcm_organization_id_idx": { + "name": "kcm_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_credential_unique": { + "name": "kcm_connector_credential_unique", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_queue_idx": { + "name": "kcm_connector_queue_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "last_started_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_credential_idx": { + "name": "kcm_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_workspace_id_workspace_id_fk": { + "name": "knowledge_connector_member_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_organization_id_organization_id_fk": { + "name": "knowledge_connector_member_organization_id_organization_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_credential_id_credential_id_fk": { + "name": "knowledge_connector_member_credential_id_credential_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcm_owner_check": { + "name": "kcm_owner_check", + "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1" + }, + "kcm_status_check": { + "name": "kcm_status_check", + "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')" + }, + "kcm_subject_token_shape_check": { + "name": "kcm_subject_token_shape_check", + "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member_sync_log": { + "name": "knowledge_connector_member_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "members_claimed": { + "name": "members_claimed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_completed": { + "name": "members_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_incomplete": { + "name": "members_incomplete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_failed": { + "name": "members_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processing_dispatch_failed": { + "name": "processing_dispatch_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "docs_listed": { + "name": "docs_listed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_hydrated_once": { + "name": "docs_hydrated_once", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_added": { + "name": "observations_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_renewed": { + "name": "observations_renewed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_removed": { + "name": "observations_removed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_tombstoned": { + "name": "docs_tombstoned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_resurrected": { + "name": "docs_resurrected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_purged": { + "name": "docs_purged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credentials_audited": { + "name": "credentials_audited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_failure_class": { + "name": "database_failure_class", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcmsl_connector_started_at_idx": { + "name": "kcmsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcmsl_started_at_partial_idx": { + "name": "kcmsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcmsl_status_check": { + "name": "kcmsl_status_check", + "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')" + }, + "kcmsl_database_failure_class_check": { + "name": "kcmsl_database_failure_class_check", + "value": "\"knowledge_connector_member_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_partition": { + "name": "knowledge_connector_partition", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "partition_key": { + "name": "partition_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_at": { + "name": "retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_served_at": { + "name": "last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure": { + "name": "failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "permission_cursor": { + "name": "permission_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_attempts": { + "name": "permission_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "permission_retry_at": { + "name": "permission_retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permission_last_served_at": { + "name": "permission_last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_started_at": { + "name": "permission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_failure": { + "name": "permission_failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcp_content_due_idx": { + "name": "kcp_content_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcp_permission_due_idx": { + "name": "kcp_permission_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_partition_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_partition_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_partition", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcp_pk": { + "name": "kcp_pk", + "columns": ["connector_id", "partition_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcp_partition_key_check": { + "name": "kcp_partition_key_check", + "value": "octet_length(\"knowledge_connector_partition\".\"partition_key\") BETWEEN 1 AND 1024" + }, + "kcp_context_check": { + "name": "kcp_context_check", + "value": "jsonb_typeof(\"knowledge_connector_partition\".\"context\") = 'object' AND octet_length(\"knowledge_connector_partition\".\"context\"::text) <= 16384" + }, + "kcp_status_check": { + "name": "kcp_status_check", + "value": "\"knowledge_connector_partition\".\"status\" IN ('pending', 'complete', 'blocked')" + }, + "kcp_cursor_check": { + "name": "kcp_cursor_check", + "value": "(\"knowledge_connector_partition\".\"cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"cursor\") <= 393216) AND (\"knowledge_connector_partition\".\"permission_cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"permission_cursor\") <= 393216)" + }, + "kcp_attempts_check": { + "name": "kcp_attempts_check", + "value": "\"knowledge_connector_partition\".\"attempts\" >= 0 AND \"knowledge_connector_partition\".\"permission_attempts\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_grant": { + "name": "knowledge_connector_permission_grant", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kcpg_subject_idx": { + "name": "kcpg_subject_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kcpg_snapshot_fk": { + "name": "kcpg_snapshot_fk", + "tableFrom": "knowledge_connector_permission_grant", + "tableTo": "knowledge_connector_permission_snapshot", + "columnsFrom": ["connector_id"], + "columnsTo": ["connector_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcpg_pk": { + "name": "kcpg_pk", + "columns": ["connector_id", "group_key", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcpg_group_check": { + "name": "kcpg_group_check", + "value": "length(\"knowledge_connector_permission_grant\".\"group_key\") BETWEEN 1 AND 255" + }, + "kcpg_subject_check": { + "name": "kcpg_subject_check", + "value": "\"knowledge_connector_permission_grant\".\"subject_token\" ~ '^u:[^[:space:]A-Z]+@[^[:space:]A-Z]+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_snapshot": { + "name": "knowledge_connector_permission_snapshot", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "kcps_connector_fk": { + "name": "kcps_connector_fk", + "tableFrom": "knowledge_connector_permission_snapshot", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcps_revision_check": { + "name": "kcps_revision_check", + "value": "\"knowledge_connector_permission_snapshot\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_skipped": { + "name": "docs_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "listed_count": { + "name": "listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_failure_class": { + "name": "database_failure_class", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_terminal_started_at_idx": { + "name": "kcsl_terminal_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" IN ('completed', 'failed')", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcsl_database_failure_class_check": { + "name": "kcsl_database_failure_class_check", + "value": "\"knowledge_connector_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_document_observation": { + "name": "knowledge_document_observation", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kdo_member_idx": { + "name": "kdo_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_document_observation_document_id_document_id_fk": { + "name": "knowledge_document_observation_document_id_document_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": { + "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "knowledge_connector_member", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_document_observation_document_id_member_id_pk": { + "name": "knowledge_document_observation_document_id_member_id_pk", + "columns": ["document_id", "member_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_external_directory": { + "name": "knowledge_external_directory", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_sync_at": { + "name": "last_complete_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ked_organization_id_idx": { + "name": "ked_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_workspace_identity_unique": { + "name": "ked_workspace_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_organization_identity_unique": { + "name": "ked_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_directory_workspace_id_workspace_id_fk": { + "name": "knowledge_external_directory_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_external_directory_organization_id_organization_id_fk": { + "name": "knowledge_external_directory_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ked_owner_check": { + "name": "ked_owner_check", + "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group": { + "name": "knowledge_external_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_group_id": { + "name": "external_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keg_organization_id_idx": { + "name": "keg_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_identity_unique": { + "name": "keg_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_synced_idx": { + "name": "keg_organization_synced_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_identity_unique": { + "name": "keg_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_workspace_synced_idx": { + "name": "keg_workspace_synced_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_group_organization_id_organization_id_fk": { + "name": "knowledge_external_group_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "keg_workspace_fk": { + "name": "keg_workspace_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "keg_owner_check": { + "name": "keg_owner_check", + "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group_member": { + "name": "knowledge_external_group_member", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kegm_subject_token_idx": { + "name": "kegm_subject_token_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kegm_group_fk": { + "name": "kegm_group_fk", + "tableFrom": "knowledge_external_group_member", + "tableTo": "knowledge_external_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_external_group_member_group_id_subject_token_pk": { + "name": "knowledge_external_group_member_group_id_subject_token_pk", + "columns": ["group_id", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_projection_dirty": { + "name": "knowledge_projection_dirty", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "content": { + "name": "content", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marked_at": { + "name": "marked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "knowledge_projection_dirty_marked_at_idx": { + "name": "knowledge_projection_dirty_marked_at_idx", + "columns": [ + { + "expression": "marked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_projection_dirty_document_id_document_id_fk": { + "name": "knowledge_projection_dirty_document_id_document_id_fk", + "tableFrom": "knowledge_projection_dirty", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_organization_id_organization_id_fk": { + "name": "mcp_server_oauth_organization_id_organization_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_server_oauth_owner_check": { + "name": "mcp_server_oauth_owner_check", + "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_connector_id": { + "name": "managed_connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config_version": { + "name": "oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_organization_id_idx": { + "name": "mcp_servers_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_idx": { + "name": "mcp_servers_credential_group_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_managed_connector_unique": { + "name": "mcp_servers_credential_group_managed_connector_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "managed_connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_organization_id_organization_id_fk": { + "name": "mcp_servers_organization_id_organization_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_credential_group_id_credential_group_id_fk": { + "name": "mcp_servers_credential_group_id_credential_group_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_servers_owner_check": { + "name": "mcp_servers_owner_check", + "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1" + }, + "mcp_servers_organization_managed_check": { + "name": "mcp_servers_organization_managed_check", + "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL" + }, + "mcp_servers_credential_group_managed_connector_check": { + "name": "mcp_servers_credential_group_managed_connector_check", + "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL" + }, + "mcp_servers_managed_connector_oauth_check": { + "name": "mcp_servers_managed_connector_oauth_check", + "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'" + } + }, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "storage_version": { + "name": "storage_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "encrypted_context_summary": { + "name": "encrypted_context_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_artifact": { + "name": "memory_artifact", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "memory_artifact_key_idx": { + "name": "memory_artifact_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_artifact_memory_id_memory_id_fk": { + "name": "memory_artifact_memory_id_memory_id_fk", + "tableFrom": "memory_artifact", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_artifact_key_execution_large_values_key_fk": { + "name": "memory_artifact_key_execution_large_values_key_fk", + "tableFrom": "memory_artifact", + "tableTo": "execution_large_values", + "columnsFrom": ["key"], + "columnsTo": ["key"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memory_artifact_memory_id_key_pk": { + "name": "memory_artifact_memory_id_key_pk", + "columns": ["memory_id", "key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_item": { + "name": "memory_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "memory_item_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "append_key": { + "name": "append_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_status": { + "name": "provenance_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_entries": { + "name": "provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memory_item_append_unique": { + "name": "memory_item_append_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "append_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_item_sequence_idx": { + "name": "memory_item_sequence_idx", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_item_memory_id_memory_id_fk": { + "name": "memory_item_memory_id_memory_id_fk", + "tableFrom": "memory_item", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_item_kind_check": { + "name": "memory_item_kind_check", + "value": "\"memory_item\".\"kind\" IN ('message', 'exchange')" + }, + "memory_item_provenance_status_check": { + "name": "memory_item_provenance_status_check", + "value": "\"memory_item\".\"provenance_status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_benchmark_runs": { + "name": "mothership_benchmark_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "benchmark_id": { + "name": "benchmark_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "evaluation_key": { + "name": "evaluation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "correct": { + "name": "correct", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "automatic_correct": { + "name": "automatic_correct", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total": { + "name": "total", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_benchmark_runs_benchmark_created_idx": { + "name": "mothership_benchmark_runs_benchmark_created_idx", + "columns": [ + { + "expression": "benchmark_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_benchmark_runs_benchmark_id_mothership_benchmarks_id_fk": { + "name": "mothership_benchmark_runs_benchmark_id_mothership_benchmarks_id_fk", + "tableFrom": "mothership_benchmark_runs", + "tableTo": "mothership_benchmarks", + "columnsFrom": ["benchmark_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mothership_benchmark_runs_score_check": { + "name": "mothership_benchmark_runs_score_check", + "value": "\"mothership_benchmark_runs\".\"total\" > 0 AND \"mothership_benchmark_runs\".\"correct\" >= 0 AND \"mothership_benchmark_runs\".\"correct\" <= \"mothership_benchmark_runs\".\"total\"" + }, + "mothership_benchmark_runs_version_check": { + "name": "mothership_benchmark_runs_version_check", + "value": "\"mothership_benchmark_runs\".\"version\" > 0" + }, + "mothership_benchmark_runs_automatic_score_check": { + "name": "mothership_benchmark_runs_automatic_score_check", + "value": "\"mothership_benchmark_runs\".\"automatic_correct\" >= 0 AND \"mothership_benchmark_runs\".\"automatic_correct\" <= \"mothership_benchmark_runs\".\"total\"" + } + }, + "isRLSEnabled": false + }, + "public.mothership_benchmarks": { + "name": "mothership_benchmarks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_as_user_id": { + "name": "run_as_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "running_stage": { + "name": "running_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "planner_chat_id": { + "name": "planner_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_benchmarks_owner_created_idx": { + "name": "mothership_benchmarks_owner_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mothership_benchmarks_workspace_idx": { + "name": "mothership_benchmarks_workspace_idx", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_benchmarks_organization_id_organization_id_fk": { + "name": "mothership_benchmarks_organization_id_organization_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_user_id_user_id_fk": { + "name": "mothership_benchmarks_user_id_user_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_run_as_user_id_user_id_fk": { + "name": "mothership_benchmarks_run_as_user_id_user_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "user", + "columnsFrom": ["run_as_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_source_workspace_id_workspace_id_fk": { + "name": "mothership_benchmarks_source_workspace_id_workspace_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mothership_benchmarks_version_check": { + "name": "mothership_benchmarks_version_check", + "value": "\"mothership_benchmarks\".\"version\" > 0" + }, + "mothership_benchmarks_attempt_check": { + "name": "mothership_benchmarks_attempt_check", + "value": "(\"mothership_benchmarks\".\"running_stage\" IS NULL AND \"mothership_benchmarks\".\"attempt_id\" IS NULL AND \"mothership_benchmarks\".\"lease_expires_at\" IS NULL) OR (\"mothership_benchmarks\".\"running_stage\" IS NOT NULL AND \"mothership_benchmarks\".\"running_stage\" IN ('distill', 'redact', 'plan', 'reconstruct', 'grade') AND \"mothership_benchmarks\".\"attempt_id\" IS NOT NULL AND \"mothership_benchmarks\".\"lease_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_memory_selections": { + "name": "mothership_memory_selections", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_memory_selections_user_id_user_id_fk": { + "name": "mothership_memory_selections_user_id_user_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_selections_organization_id_organization_id_fk": { + "name": "mothership_memory_selections_organization_id_organization_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_selections_space_id_mothership_memory_spaces_id_fk": { + "name": "mothership_memory_selections_space_id_mothership_memory_spaces_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "mothership_memory_spaces", + "columnsFrom": ["space_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mothership_memory_selections_user_id_organization_id_pk": { + "name": "mothership_memory_selections_user_id_organization_id_pk", + "columns": ["user_id", "organization_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_memory_spaces": { + "name": "mothership_memory_spaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_memory_spaces_owner_idx": { + "name": "mothership_memory_spaces_owner_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_memory_spaces_user_id_user_id_fk": { + "name": "mothership_memory_spaces_user_id_user_id_fk", + "tableFrom": "mothership_memory_spaces", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_spaces_organization_id_organization_id_fk": { + "name": "mothership_memory_spaces_organization_id_organization_id_fk", + "tableFrom": "mothership_memory_spaces", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_resource_effects": { + "name": "mothership_resource_effects", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effect_id": { + "name": "effect_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_resource_effects_chat_id_copilot_chats_id_fk": { + "name": "mothership_resource_effects_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_resource_effects", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mothership_resource_effects_chat_id_effect_id_pk": { + "name": "mothership_resource_effects_chat_id_effect_id_pk", + "columns": ["chat_id", "effect_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_session_id_idx": { + "name": "oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_id_idx": { + "name": "oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_user_client_idx": { + "name": "oauth_access_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": ["refresh_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_access_token_search_resource_check": { + "name": "oauth_access_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_access_token\".\"scopes\")) OR \"oauth_access_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_client_user_id_idx": { + "name": "oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_consent_user_client_reference_unique": { + "name": "oauth_consent_user_client_reference_unique", + "nullsNotDistinct": true, + "columns": ["user_id", "client_id", "reference_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_id": { + "name": "family_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_refresh_token_client_id_idx": { + "name": "oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_session_id_idx": { + "name": "oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_user_client_idx": { + "name": "oauth_refresh_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_expires_at_idx": { + "name": "oauth_refresh_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_family_id_oauth_token_family_id_fk": { + "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_token_family", + "columnsFrom": ["family_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + }, + "oauth_refresh_token_family_generation_unique": { + "name": "oauth_refresh_token_family_generation_unique", + "nullsNotDistinct": false, + "columns": ["family_id", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_refresh_token_generation_check": { + "name": "oauth_refresh_token_generation_check", + "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000" + }, + "oauth_refresh_token_search_resource_check": { + "name": "oauth_refresh_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_refresh_token\".\"scopes\")) OR \"oauth_refresh_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_token_family": { + "name": "oauth_token_family", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_generation": { + "name": "current_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_token_family_client_id_idx": { + "name": "oauth_token_family_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_session_id_idx": { + "name": "oauth_token_family_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_user_client_idx": { + "name": "oauth_token_family_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_consent_id_idx": { + "name": "oauth_token_family_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_expires_at_idx": { + "name": "oauth_token_family_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_token_family_client_id_oauth_client_client_id_fk": { + "name": "oauth_token_family_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_session_id_session_id_fk": { + "name": "oauth_token_family_session_id_session_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_token_family_user_id_user_id_fk": { + "name": "oauth_token_family_user_id_user_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_consent_id_oauth_consent_id_fk": { + "name": "oauth_token_family_consent_id_oauth_consent_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_consent", + "columnsFrom": ["consent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "oauth_token_family_generation_check": { + "name": "oauth_token_family_generation_check", + "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000" + } + }, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "require_sso": { + "name": "require_sso", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_access_request_settings": { + "name": "organization_access_request_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "allow_requests": { + "name": "allow_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_access_request_settings_organization_id_organization_id_fk": { + "name": "organization_access_request_settings_organization_id_organization_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_access_request_settings_updated_by_user_id_fk": { + "name": "organization_access_request_settings_updated_by_user_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_byok_keys": { + "name": "organization_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_byok_organization_provider_idx": { + "name": "organization_byok_organization_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_byok_keys_organization_id_organization_id_fk": { + "name": "organization_byok_keys_organization_id_organization_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_byok_keys_created_by_user_id_fk": { + "name": "organization_byok_keys_created_by_user_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_history": { + "name": "organization_search_history", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "queries": { + "name": "queries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "organization_search_history_user_idx": { + "name": "organization_search_history_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_search_history_organization_id_organization_id_fk": { + "name": "organization_search_history_organization_id_organization_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_search_history_user_id_user_id_fk": { + "name": "organization_search_history_user_id_user_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_history_organization_id_user_id_pk": { + "name": "organization_search_history_organization_id_user_id_pk", + "columns": ["organization_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_integration": { + "name": "organization_search_integration", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_search_integration_organization_id_organization_id_fk": { + "name": "organization_search_integration_organization_id_organization_id_fk", + "tableFrom": "organization_search_integration", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_integration_organization_id_connector_type_pk": { + "name": "organization_search_integration_organization_id_connector_type_pk", + "columns": ["organization_id", "connector_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_mcp_invocation": { + "name": "organization_search_mcp_invocation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_search_mcp_invocation_org_created_idx": { + "name": "organization_search_mcp_invocation_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_search_mcp_invocation_user_idx": { + "name": "organization_search_mcp_invocation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_search_mcp_invocation_org_fk": { + "name": "org_search_mcp_invocation_org_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_search_mcp_invocation_user_fk": { + "name": "org_search_mcp_invocation_user_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_search_mcp_invocation_tool_check": { + "name": "organization_search_mcp_invocation_tool_check", + "value": "\"organization_search_mcp_invocation\".\"tool_name\" IN ('search', 'read_document', 'chat')" + }, + "organization_search_mcp_invocation_outcome_check": { + "name": "organization_search_mcp_invocation_outcome_check", + "value": "\"organization_search_mcp_invocation\".\"outcome\" IN ('success', 'error', 'cancelled', 'rate_limited')" + }, + "organization_search_mcp_invocation_duration_check": { + "name": "organization_search_mcp_invocation_duration_check", + "value": "\"organization_search_mcp_invocation\".\"duration_ms\" >= 0" + }, + "organization_search_mcp_invocation_client_name_check": { + "name": "organization_search_mcp_invocation_client_name_check", + "value": "length(\"organization_search_mcp_invocation\".\"client_name\") <= 256" + }, + "organization_search_mcp_invocation_auth_check": { + "name": "organization_search_mcp_invocation_auth_check", + "value": "(\"organization_search_mcp_invocation\".\"auth_kind\" = 'oauth_access_token' AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NOT NULL)\n OR (\"organization_search_mcp_invocation\".\"auth_kind\" IN ('personal_api_key', 'workspace_api_key') AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NULL AND \"organization_search_mcp_invocation\".\"client_name\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.organization_secret": { + "name": "organization_secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_shared_unique": { + "name": "organization_secret_shared_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_member_unique": { + "name": "organization_secret_member_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_owner_idx": { + "name": "organization_secret_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_id_organization_secret_source_id_fk": { + "name": "organization_secret_source_id_organization_secret_source_id_fk", + "tableFrom": "organization_secret", + "tableTo": "organization_secret_source", + "columnsFrom": ["source_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_secret_owner_user_id_user_id_fk": { + "name": "organization_secret_owner_user_id_user_id_fk", + "tableFrom": "organization_secret", + "tableTo": "user", + "columnsFrom": ["owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_secret_source": { + "name": "organization_secret_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_source_org_unique": { + "name": "organization_secret_source_org_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_organization_id_organization_id_fk": { + "name": "organization_secret_source_organization_id_organization_id_fk", + "tableFrom": "organization_secret_source", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_secret_source_mode_check": { + "name": "organization_secret_source_mode_check", + "value": "\"organization_secret_source\".\"mode\" IN ('organization', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_pending_type_available_idx": { + "name": "outbox_event_pending_type_available_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_event\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_organization_id_idx": { + "name": "pending_draft_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_org": { + "name": "pending_draft_user_provider_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_organization_id_organization_id_fk": { + "name": "pending_credential_draft_organization_id_organization_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_draft_owner_check": { + "name": "pending_draft_owner_check", + "value": "num_nonnulls(\"pending_credential_draft\".\"workspace_id\", \"pending_credential_draft\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.permission_access_request": { + "name": "permission_access_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_id": { + "name": "requester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "target_label": { + "name": "target_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decision_reason": { + "name": "decision_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by": { + "name": "decided_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "permission_access_request_pending_unique": { + "name": "permission_access_request_pending_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"permission_access_request\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_org_queue_idx": { + "name": "permission_access_request_org_queue_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_requester_idx": { + "name": "permission_access_request_requester_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_access_request_organization_id_organization_id_fk": { + "name": "permission_access_request_organization_id_organization_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_requester_id_user_id_fk": { + "name": "permission_access_request_requester_id_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["requester_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_decided_by_user_id_fk": { + "name": "permission_access_request_decided_by_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["decided_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "permission_access_request_status_check": { + "name": "permission_access_request_status_check", + "value": "\"permission_access_request\".\"status\" in ('pending', 'fulfilled', 'declined', 'cancelled', 'closed')" + } + }, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "membership_mode": { + "name": "membership_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_scope_key": { + "name": "organization_scope_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END", + "type": "stored" + } + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_organization_archive_id_idx": { + "name": "project_organization_archive_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_archive_id_idx": { + "name": "project_owner_archive_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_organization_id_organization_id_fk": { + "name": "project_organization_id_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "project_owner_id_user_id_fk": { + "name": "project_owner_id_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_id_organization_scope_unique": { + "name": "project_id_organization_scope_unique", + "nullsNotDistinct": false, + "columns": ["id", "organization_scope_key"] + } + }, + "policies": {}, + "checkConstraints": { + "project_name_length": { + "name": "project_name_length", + "value": "char_length(btrim(\"project\".\"name\")) BETWEEN 1 AND 100" + } + }, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_entity_id_idx": { + "name": "public_share_entity_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "public_share_entity_binding_check": { + "name": "public_share_entity_binding_check", + "value": "\n (\"public_share\".\"entity_type\" IS NULL AND \"public_share\".\"entity_id\" IS NULL AND \"public_share\".\"workspace_id\" IS NOT NULL)\n OR (\"public_share\".\"entity_type\" IS NOT NULL AND \"public_share\".\"entity_id\" IS NOT NULL AND char_length(\"public_share\".\"entity_id\") > 0\n AND ((\"public_share\".\"entity_type\" = 'workspace' AND \"public_share\".\"workspace_id\" IS NOT NULL AND \"public_share\".\"entity_id\" = \"public_share\".\"workspace_id\")\n OR (\"public_share\".\"entity_type\" = 'project' AND \"public_share\".\"workspace_id\" IS NULL AND \"public_share\".\"resource_type\" = 'file')))\n " + } + }, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_state": { + "name": "capacity_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_policy": { + "name": "resource_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "resource_policy_organization_id_idx": { + "name": "resource_policy_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_resource_unique": { + "name": "resource_policy_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_workspace_id_idx": { + "name": "resource_policy_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_policy_workspace_id_workspace_id_fk": { + "name": "resource_policy_workspace_id_workspace_id_fk", + "tableFrom": "resource_policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_organization_id_organization_id_fk": { + "name": "resource_policy_organization_id_organization_id_fk", + "tableFrom": "resource_policy", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_created_by_user_id_fk": { + "name": "resource_policy_created_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_policy_updated_by_user_id_fk": { + "name": "resource_policy_updated_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "resource_policy_owner_check": { + "name": "resource_policy_owner_check", + "value": "num_nonnulls(\"resource_policy\".\"workspace_id\", \"resource_policy\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection": { + "name": "scim_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconcile_lock_token": { + "name": "reconcile_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconcile_lease_at": { + "name": "reconcile_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciled_at": { + "name": "reconciled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_connection_organization_unique": { + "name": "scim_connection_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_connection_reconcile_due_idx": { + "name": "scim_connection_reconcile_due_idx", + "columns": [ + { + "expression": "reconciled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_connection_organization_id_organization_id_fk": { + "name": "scim_connection_organization_id_organization_id_fk", + "tableFrom": "scim_connection", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_connection_created_by_user_id_fk": { + "name": "scim_connection_created_by_user_id_fk", + "tableFrom": "scim_connection", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_credential": { + "name": "scim_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_credential_token_hash_unique": { + "name": "scim_credential_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_credential_connection_idx": { + "name": "scim_credential_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_credential_connection_id_scim_connection_id_fk": { + "name": "scim_credential_connection_id_scim_connection_id_fk", + "tableFrom": "scim_credential", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_credential_revoked_by_user_id_fk": { + "name": "scim_credential_revoked_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scim_credential_created_by_user_id_fk": { + "name": "scim_credential_created_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_connection_display_name_unique": { + "name": "scim_group_connection_display_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_external_id_unique": { + "name": "scim_group_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_order_idx": { + "name": "scim_group_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_connection_id_scim_connection_id_fk": { + "name": "scim_group_connection_id_scim_connection_id_fk", + "tableFrom": "scim_group", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_mapping": { + "name": "scim_group_mapping", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_mapping_group_idx": { + "name": "scim_group_mapping_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_permission_group_idx": { + "name": "scim_group_mapping_permission_group_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_workspace_idx": { + "name": "scim_group_mapping_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_group_target_unique": { + "name": "scim_group_mapping_group_target_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"permission_group_id\", \"workspace_id\", \"role\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_mapping_group_id_scim_group_id_fk": { + "name": "scim_group_mapping_group_id_scim_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_permission_group_id_permission_group_id_fk": { + "name": "scim_group_mapping_permission_group_id_permission_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_workspace_id_workspace_id_fk": { + "name": "scim_group_mapping_workspace_id_workspace_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_created_by_user_id_fk": { + "name": "scim_group_mapping_created_by_user_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scim_group_mapping_target_shape": { + "name": "scim_group_mapping_target_shape", + "value": "(\n (\"scim_group_mapping\".\"target_kind\" = 'permission_group' AND \"scim_group_mapping\".\"permission_group_id\" IS NOT NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'workspace' AND \"scim_group_mapping\".\"workspace_id\" IS NOT NULL AND \"scim_group_mapping\".\"permission_type\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'org_role' AND \"scim_group_mapping\".\"role\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_member_group_user_unique": { + "name": "scim_group_member_group_user_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_member_scim_user_idx": { + "name": "scim_group_member_scim_user_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "baseline_permission": { + "name": "baseline_permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'directory'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_projection_grant_user_target_unique": { + "name": "scim_projection_grant_user_target_unique", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grant_connection_idx": { + "name": "scim_projection_grant_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_connection_id_scim_connection_id_fk": { + "name": "scim_projection_grant_connection_id_scim_connection_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_request_log": { + "name": "scim_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scim_type": { + "name": "scim_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_request_log_connection_created_idx": { + "name": "scim_request_log_connection_created_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_request_log_connection_id_scim_connection_id_fk": { + "name": "scim_request_log_connection_id_scim_connection_id_fk", + "tableFrom": "scim_request_log", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_connection_user_unique": { + "name": "scim_user_connection_user_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_user_name_unique": { + "name": "scim_user_connection_user_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_external_id_unique": { + "name": "scim_user_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_order_idx": { + "name": "scim_user_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_user_idx": { + "name": "scim_user_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_connection_id_scim_connection_id_fk": { + "name": "scim_user_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user_tombstone": { + "name": "scim_user_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_tombstone_connection_external_id_unique": { + "name": "scim_user_tombstone_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_tombstone_user_idx": { + "name": "scim_user_tombstone_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_tombstone_connection_id_scim_connection_id_fk": { + "name": "scim_user_tombstone_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_tombstone_user_id_user_id_fk": { + "name": "scim_user_tombstone_user_id_user_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_usage": { + "name": "secret_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_name": { + "name": "secret_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_scope": { + "name": "secret_scope", + "type": "secret_usage_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret_owner_user_id": { + "name": "secret_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source": { + "name": "source", + "type": "secret_usage_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_execution_id": { + "name": "last_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_trigger": { + "name": "last_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_usage_bucket_unique": { + "name": "secret_usage_bucket_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_usage_secret_recent_idx": { + "name": "secret_usage_secret_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_usage_workspace_id_workspace_id_fk": { + "name": "secret_usage_workspace_id_workspace_id_fk", + "tableFrom": "secret_usage", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_focus_on_click": { + "name": "auto_focus_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_app": { + "name": "slack_app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_client_secret": { + "name": "encrypted_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_signing_secret": { + "name": "encrypted_signing_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "slack_app_organization_id_organization_id_fk": { + "name": "slack_app_organization_id_organization_id_fk", + "tableFrom": "slack_app", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_app_owner_check": { + "name": "slack_app_owner_check", + "value": "(\"slack_app\".\"kind\" = 'custom' AND \"slack_app\".\"organization_id\" IS NOT NULL) OR (\"slack_app\".\"kind\" = 'shared' AND \"slack_app\".\"organization_id\" IS NULL)" + }, + "slack_app_custom_credentials_check": { + "name": "slack_app_custom_credentials_check", + "value": "\"slack_app\".\"kind\" = 'shared' OR (\"slack_app\".\"client_id\" IS NOT NULL AND \"slack_app\".\"encrypted_client_secret\" IS NOT NULL AND \"slack_app\".\"encrypted_signing_secret\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.slack_search_installation": { + "name": "slack_search_installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "credential_version": { + "name": "credential_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_outcome": { + "name": "last_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_installation_organization_idx": { + "name": "slack_search_installation_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_credential_unique": { + "name": "slack_search_installation_credential_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_app_team_unique": { + "name": "slack_search_installation_app_team_unique", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_active_team_unique": { + "name": "slack_search_installation_active_team_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_installation\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_installation_organization_id_organization_id_fk": { + "name": "slack_search_installation_organization_id_organization_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_credential_id_credential_id_fk": { + "name": "slack_search_installation_credential_id_credential_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_slack_app_id_slack_app_id_fk": { + "name": "slack_search_installation_slack_app_id_slack_app_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_search_turn": { + "name": "slack_search_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "slack_search_turn_ordinal_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_key": { + "name": "conversation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "lease_id": { + "name": "lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_turn_event_unique": { + "name": "slack_search_turn_event_unique", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_pending_idx": { + "name": "slack_search_turn_pending_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_thread_idx": { + "name": "slack_search_turn_thread_idx", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_active_thread_unique": { + "name": "slack_search_turn_active_thread_unique", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_turn\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_turn_installation_id_slack_search_installation_id_fk": { + "name": "slack_search_turn_installation_id_slack_search_installation_id_fk", + "tableFrom": "slack_search_turn", + "tableTo": "slack_search_installation", + "columnsFrom": ["installation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "primary_provider_id": { + "name": "primary_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_organization_id_idx": { + "name": "sso_domain_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "jit_provisioning_enabled": { + "name": "jit_provisioning_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "last_closed_period_start": { + "name": "last_closed_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_cycle_close_lagging_idx": { + "name": "subscription_cycle_close_lagging_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_capability_governed_user_id_user_id_fk": { + "name": "table_row_executions_capability_governed_user_id_user_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_governed_active_idx": { + "name": "table_run_dispatches_governed_active_idx", + "columns": [ + { + "expression": "capability_governed_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_run_dispatches\".\"status\" IN ('pending', 'dispatching')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "table_run_dispatches_capability_governed_user_id_user_id_fk": { + "name": "table_run_dispatches_capability_governed_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_workspace_created_idx": { + "name": "table_views_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_period_cost_idx": { + "name": "usage_log_billing_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspension_source": { + "name": "suspension_source", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_lower_idx": { + "name": "user_email_lower_idx", + "columns": [ + { + "expression": "lower(btrim(\"email\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_created_by_idx": { + "name": "user_table_def_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_changes": { + "name": "user_table_row_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_table_row_changes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_delta": { + "name": "row_delta", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_table_row_changes_table_id_idx": { + "name": "user_table_row_changes_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_row_changes_table_id_user_table_definitions_id_fk": { + "name": "user_table_row_changes_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_row_changes", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_created_by_idx": { + "name": "user_table_rows_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_rows\".\"created_by\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": { + "fastupdate": "off" + } + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_created_id_idx": { + "name": "user_table_rows_table_created_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_active_workspace_sort_idx": { + "name": "workflow_active_workspace_sort_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_activity_level_idx": { + "name": "workflow_execution_logs_workspace_activity_level_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "total_duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_started_at_idx": { + "name": "workflow_execution_logs_redacting_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'redacting'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_deadline_idx": { + "name": "workflow_execution_logs_redacting_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_test": { + "name": "workflow_test", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_file_id": { + "name": "body_file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cases": { + "name": "cases", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_test_workspace_name_unique": { + "name": "workflow_test_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_test\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_test_body_file_unique": { + "name": "workflow_test_body_file_unique", + "columns": [ + { + "expression": "body_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_test_workspace_id_workspace_id_fk": { + "name": "workflow_test_workspace_id_workspace_id_fk", + "tableFrom": "workflow_test", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_body_file_id_workspace_files_id_fk": { + "name": "workflow_test_body_file_id_workspace_files_id_fk", + "tableFrom": "workflow_test", + "tableTo": "workspace_files", + "columnsFrom": ["body_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_test_created_by_user_id_user_id_fk": { + "name": "workflow_test_created_by_user_id_user_id_fk", + "tableFrom": "workflow_test", + "tableTo": "user", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_test_run": { + "name": "workflow_test_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "test_id": { + "name": "test_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "passed": { + "name": "passed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped": { + "name": "skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ran_against": { + "name": "ran_against", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "triggered_by_actor": { + "name": "triggered_by_actor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_test_run_test_version_started_idx": { + "name": "workflow_test_run_test_version_started_idx", + "columns": [ + { + "expression": "test_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_test_run_test_id_workflow_test_id_fk": { + "name": "workflow_test_run_test_id_workflow_test_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "workflow_test", + "columnsFrom": ["test_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_run_workspace_id_workspace_id_fk": { + "name": "workflow_test_run_workspace_id_workspace_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_run_triggered_by_user_id_user_id_fk": { + "name": "workflow_test_run_triggered_by_user_id_user_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_test_run_version_check": { + "name": "workflow_test_run_version_check", + "value": "\"workflow_test_run\".\"version\" IN ('draft', 'deployed')" + }, + "workflow_test_run_status_check": { + "name": "workflow_test_run_status_check", + "value": "\"workflow_test_run\".\"status\" IN ('running', 'passed', 'failed', 'error')" + }, + "workflow_test_run_completed_check": { + "name": "workflow_test_run_completed_check", + "value": "(\"workflow_test_run\".\"status\" = 'running') = (\"workflow_test_run\".\"completed_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_scope_key": { + "name": "organization_scope_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END", + "type": "stored" + } + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fork_sync_new_workflows_excluded": { + "name": "fork_sync_new_workflows_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_project_id_id_idx": { + "name": "workspace_project_id_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_project_id_project_id_fk": { + "name": "workspace_project_id_project_id_fk", + "tableFrom": "workspace", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_project_organization_fk": { + "name": "workspace_project_organization_fk", + "tableFrom": "workspace", + "tableTo": "project", + "columnsFrom": ["project_id", "organization_scope_key"], + "columnsTo": ["id", "organization_scope_key"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_fork_project_fk": { + "name": "workspace_fork_project_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id", "project_id"], + "columnsTo": ["id", "project_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_id_project_unique": { + "name": "workspace_id_project_unique", + "nullsNotDistinct": false, + "columns": ["id", "project_id"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_uploaded_by_idx": { + "name": "workspace_file_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_backfill": { + "name": "workspace_file_search_backfill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "after_workspace_id": { + "name": "after_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_entity_type": { + "name": "after_entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_entity_id": { + "name": "after_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_file_id": { + "name": "after_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_build": { + "name": "workspace_file_search_build", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_file_search_build_file_idx": { + "name": "workspace_file_search_build_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_build_cleanup_idx": { + "name": "workspace_file_search_build_cleanup_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_build\".\"expires_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_build_owner_check": { + "name": "file_search_build_owner_check", + "value": "(\"workspace_file_search_build\".\"entity_type\" IS NULL AND \"workspace_file_search_build\".\"entity_id\" IS NULL AND \"workspace_file_search_build\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_build\".\"entity_type\" = 'workspace' AND \"workspace_file_search_build\".\"entity_id\" = \"workspace_file_search_build\".\"workspace_id\") OR (\"workspace_file_search_build\".\"entity_type\" = 'project' AND length(\"workspace_file_search_build\".\"entity_id\") > 0 AND \"workspace_file_search_build\".\"workspace_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_chunk": { + "name": "workspace_file_search_chunk", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_start": { + "name": "line_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fragment": { + "name": "fragment", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "overlap": { + "name": "overlap", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "file_search_chunk_owner_content_idx": { + "name": "file_search_chunk_owner_content_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_chunk\".\"entity_type\" = 'project'", + "concurrently": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + }, + "workspace_file_search_chunk_line_idx": { + "name": "workspace_file_search_chunk_line_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "line_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_chunk_content_idx": { + "name": "workspace_file_search_chunk_content_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + } + }, + "foreignKeys": { + "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_chunk", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_chunk_pk": { + "name": "workspace_file_search_chunk_pk", + "columns": ["build_id", "ordinal"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_chunk_owner_check": { + "name": "file_search_chunk_owner_check", + "value": "(\"workspace_file_search_chunk\".\"entity_type\" IS NULL AND \"workspace_file_search_chunk\".\"entity_id\" IS NULL AND \"workspace_file_search_chunk\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_chunk\".\"entity_type\" = 'workspace' AND \"workspace_file_search_chunk\".\"entity_id\" = \"workspace_file_search_chunk\".\"workspace_id\") OR (\"workspace_file_search_chunk\".\"entity_type\" = 'project' AND length(\"workspace_file_search_chunk\".\"entity_id\") > 0 AND \"workspace_file_search_chunk\".\"workspace_id\" IS NULL)" + }, + "workspace_file_search_chunk_content_size": { + "name": "workspace_file_search_chunk_content_size", + "value": "octet_length(\"workspace_file_search_chunk\".\"content\") <= 8192" + }, + "workspace_file_search_chunk_position": { + "name": "workspace_file_search_chunk_position", + "value": "\"workspace_file_search_chunk\".\"ordinal\" >= 0 AND \"workspace_file_search_chunk\".\"line_start\" > 0 AND \"workspace_file_search_chunk\".\"overlap\" BETWEEN 0 AND 2" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_dispatch_queue": { + "name": "workspace_file_search_dispatch_queue", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_dispatch_queue_schedule_idx": { + "name": "workspace_file_search_dispatch_queue_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_queue_workspace_fk": { + "name": "workspace_file_search_queue_workspace_fk", + "tableFrom": "workspace_file_search_dispatch_queue", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_index": { + "name": "workspace_file_search_index", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "partial": { + "name": "partial", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_index_workspace_status_idx": { + "name": "workspace_file_search_index_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_pending_dispatch_idx": { + "name": "workspace_file_search_index_pending_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_active_dispatch_idx": { + "name": "workspace_file_search_index_active_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_index_file_fk": { + "name": "workspace_file_search_index_file_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_index_workspace_fk": { + "name": "workspace_file_search_index_workspace_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_index_pk": { + "name": "workspace_file_search_index_pk", + "columns": ["file_id", "source_content_updated_at"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_revision": { + "name": "workspace_file_search_revision", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "handoff_expires_at": { + "name": "handoff_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_search_revision_owner_pending_idx": { + "name": "file_search_revision_owner_pending_idx", + "columns": [ + { + "expression": "coalesce(\"entity_type\", 'workspace')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"entity_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "file_search_revision_owner_status_idx": { + "name": "file_search_revision_owner_status_idx", + "columns": [ + { + "expression": "coalesce(\"entity_type\", 'workspace')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"entity_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_workspace_status_idx": { + "name": "workspace_file_search_revision_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_build_idx": { + "name": "workspace_file_search_revision_build_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_pending_idx": { + "name": "workspace_file_search_revision_pending_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_active_idx": { + "name": "workspace_file_search_revision_active_idx", + "columns": [ + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_revision_file_id_workspace_files_id_fk": { + "name": "workspace_file_search_revision_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_revision_owner_check": { + "name": "file_search_revision_owner_check", + "value": "(\"workspace_file_search_revision\".\"entity_type\" IS NULL AND \"workspace_file_search_revision\".\"entity_id\" IS NULL AND \"workspace_file_search_revision\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_revision\".\"entity_type\" = 'workspace' AND \"workspace_file_search_revision\".\"entity_id\" = \"workspace_file_search_revision\".\"workspace_id\") OR (\"workspace_file_search_revision\".\"entity_type\" = 'project' AND length(\"workspace_file_search_revision\".\"entity_id\") > 0 AND \"workspace_file_search_revision\".\"workspace_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_segment": { + "name": "workspace_file_search_segment", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_number": { + "name": "segment_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_start": { + "name": "segment_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_length": { + "name": "line_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workspace_file_search_segment_workspace_revision_idx": { + "name": "workspace_file_search_segment_workspace_revision_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_segment_workspace_content_trgm_idx": { + "name": "workspace_file_search_segment_workspace_content_trgm_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_segment_file_fk": { + "name": "workspace_file_search_segment_file_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_segment_workspace_fk": { + "name": "workspace_file_search_segment_workspace_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_segment_pk": { + "name": "workspace_file_search_segment_pk", + "columns": ["file_id", "source_content_updated_at", "line_number", "segment_number"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_version": { + "name": "workspace_file_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "workspace_file_version_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "author_user_ids": { + "name": "author_user_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "restored_from_version": { + "name": "restored_from_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_status": { + "name": "secret_provenance_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_entries": { + "name": "secret_provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_version_file_version_unique": { + "name": "workspace_file_version_file_version_unique", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_key_unique": { + "name": "workspace_file_version_key_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_id_idx": { + "name": "workspace_file_version_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_superseded_idx": { + "name": "workspace_file_version_workspace_superseded_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "superseded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_version\".\"superseded_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_version_file_id_workspace_files_id_fk": { + "name": "workspace_file_version_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_version_workspace_id_workspace_id_fk": { + "name": "workspace_file_version_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_version_provenance_status_check": { + "name": "workspace_file_version_provenance_status_check", + "value": "\"workspace_file_version\".\"secret_provenance_status\" IS NULL OR \"workspace_file_version\".\"secret_provenance_status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "date_trunc('milliseconds', now())" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_project_id_idx": { + "name": "workspace_files_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_project_folder_name_active_unique": { + "name": "workspace_files_project_folder_name_active_unique", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"project_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_search_owner_keyset_idx": { + "name": "workspace_files_search_owner_keyset_idx", + "columns": [ + { + "expression": "(CASE WHEN \"project_id\" IS NOT NULL THEN 'project' ELSE 'workspace' END)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"project_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND ((\"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL) OR (\"workspace_files\".\"context\" = 'project' AND \"workspace_files\".\"project_id\" IS NOT NULL))", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_active_keyset_idx": { + "name": "workspace_files_workspace_active_keyset_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_display_name_idx": { + "name": "workspace_files_workspace_display_name_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "regexp_replace(regexp_replace(regexp_replace(normalize(\"original_name\", NFC), '^[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+|[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+$', '', 'g'), '[\\x01-\\x1f\\x7f]', '', 'g'), '[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+', ' ', 'g')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_organization_id_idx": { + "name": "workspace_files_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_project_id_project_id_fk": { + "name": "workspace_files_project_id_project_id_fk", + "tableFrom": "workspace_files", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_organization_id_organization_id_fk": { + "name": "workspace_files_organization_id_organization_id_fk", + "tableFrom": "workspace_files", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_files_owner_check": { + "name": "workspace_files_owner_check", + "value": "num_nonnulls(\"workspace_files\".\"workspace_id\", \"workspace_files\".\"project_id\", \"workspace_files\".\"organization_id\") <= 1" + }, + "workspace_files_workspace_binding_check": { + "name": "workspace_files_workspace_binding_check", + "value": "\"workspace_files\".\"context\" NOT IN ('workspace', 'chat', 'mothership', 'execution', 'workspace-logos') OR \"workspace_files\".\"workspace_id\" IS NOT NULL" + }, + "workspace_files_project_binding_check": { + "name": "workspace_files_project_binding_check", + "value": "(\"workspace_files\".\"project_id\" IS NOT NULL) = (\"workspace_files\".\"context\" = 'project') AND (\"workspace_files\".\"project_id\" IS NULL OR \"workspace_files\".\"chat_id\" IS NULL)" + }, + "workspace_files_organization_binding_check": { + "name": "workspace_files_organization_binding_check", + "value": "\"workspace_files\".\"organization_id\" IS NULL OR (\"workspace_files\".\"workspace_id\" IS NULL AND \"workspace_files\".\"context\" = 'knowledge-base' AND \"workspace_files\".\"folder_id\" IS NULL AND \"workspace_files\".\"chat_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_workflow_sync": { + "name": "workspace_fork_workflow_sync", + "schema": "", + "columns": { + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workflow_id": { + "name": "source_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_deployment_version_id": { + "name": "source_deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "workspace_fork_workflow_sync_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "promote_run_id": { + "name": "promote_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rollback_operation_id": { + "name": "rollback_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rolled_back_at": { + "name": "rolled_back_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_fork_workflow_sync_baseline_idx": { + "name": "workspace_fork_workflow_sync_baseline_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_fork_workflow_sync\".\"activated_at\" IS NOT NULL AND \"workspace_fork_workflow_sync\".\"rolled_back_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_run_idx": { + "name": "workspace_fork_workflow_sync_run_idx", + "columns": [ + { + "expression": "promote_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_rollback_idx": { + "name": "workspace_fork_workflow_sync_rollback_idx", + "columns": [ + { + "expression": "rollback_operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_fork_workflow_sync\".\"rollback_operation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_source_idx": { + "name": "workspace_fork_workflow_sync_source_idx", + "columns": [ + { + "expression": "source_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_target_idx": { + "name": "workspace_fork_workflow_sync_target_idx", + "columns": [ + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_child_workspace_idx": { + "name": "workspace_fork_workflow_sync_child_workspace_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_workflow_sync_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_workflow_sync_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_workflow_sync_source_workflow_id_workflow_id_fk": { + "name": "workspace_fork_workflow_sync_source_workflow_id_workflow_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workflow", + "columnsFrom": ["source_workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_workflow_sync_target_workflow_id_workflow_id_fk": { + "name": "workspace_fork_workflow_sync_target_workflow_id_workflow_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workflow", + "columnsFrom": ["target_workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operation_receipt": { + "name": "workspace_operation_receipt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operation_receipt_request_unique": { + "name": "workspace_operation_receipt_request_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operation_receipt_workspace_created_idx": { + "name": "workspace_operation_receipt_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operation_receipt_workspace_id_workspace_id_fk": { + "name": "workspace_operation_receipt_workspace_id_workspace_id_fk", + "tableFrom": "workspace_operation_receipt", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_workspace_idx": { + "name": "workspace_sandbox_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_visit": { + "name": "workspace_visit", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visited_at": { + "name": "visited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_visit_workspace_idx": { + "name": "workspace_visit_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_visit_user_id_user_id_fk": { + "name": "workspace_visit_user_id_user_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_visit_workspace_id_workspace_id_fk": { + "name": "workspace_visit_workspace_id_workspace_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_visit_user_id_workspace_id_pk": { + "name": "workspace_visit_user_id_workspace_id_pk", + "columns": ["user_id", "workspace_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_group_enrollment_status": { + "name": "credential_group_enrollment_status", + "schema": "public", + "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"] + }, + "public.credential_group_status": { + "name": "credential_group_status", + "schema": "public", + "values": ["active", "disabled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": [ + "oauth", + "managed_oauth", + "managed_mcp", + "env_workspace", + "env_personal", + "service_account", + "personal_token" + ] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.managed_oauth_credential_status": { + "name": "managed_oauth_credential_status", + "schema": "public", + "values": ["active", "needs_reauth", "revoked"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.secret_usage_scope": { + "name": "secret_usage_scope", + "schema": "public", + "values": ["workspace", "personal"] + }, + "public.secret_usage_source": { + "name": "secret_usage_source", + "schema": "public", + "values": ["workflow", "copilot", "mcp"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "project_file", + "table_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "organization_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool", "model_unbilled"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output", + "api-tool", + "workflow-test" + ] + }, + "public.workspace_file_search_index_status": { + "name": "workspace_file_search_index_status", + "schema": "public", + "values": ["pending", "ready", "skipped", "failed"] + }, + "public.workspace_file_version_source": { + "name": "workspace_file_version_source", + "schema": "public", + "values": ["upload", "user", "api", "copilot", "workflow", "collab", "revert", "unknown"] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "file_folder", + "mcp_server", + "workflow_mcp_server", + "custom_block", + "custom_tool", + "skill", + "sandbox" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": { + "public.copilot_async_tool_calls_persist_seq": { + "name": "copilot_async_tool_calls_persist_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index ec9b0bda3a8..cbb18e5d479 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -3011,6 +3011,20 @@ "when": 1792023339381, "tag": "0430_tool_execution_attempts", "breakpoints": true + }, + { + "idx": 431, + "version": "7", + "when": 1792093861456, + "tag": "0431_large_table_vacuum_throttle", + "breakpoints": true + }, + { + "idx": 432, + "version": "7", + "when": 1792096505838, + "tag": "0432_tool_execution_deadline", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 5f38138d796..b6cc7b761e5 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -4957,6 +4957,12 @@ export const copilotAsyncToolCalls = pgTable( executionRevokedAt: timestamp('execution_revoked_at', { withTimezone: true }), /** Sim executions started for this call, counting each re-run after its owner died. */ executionAttempts: integer('execution_attempts').notNull().default(0), + /** + * When Sim must have settled this call: the worker's wait from the frame, less the settle + * margin, fixed at first receipt. Every wait, execution and re-run of the call ends by it. + * Null when the frame named no deadline. + */ + executionDeadlineAt: timestamp('execution_deadline_at', { withTimezone: true }), /** Assigned only after the workflow HTTP executor has reserved this execution identity. */ clientWorkflowExecutionId: text('client_workflow_execution_id'), sandboxProcesses: jsonb('sandbox_processes') diff --git a/packages/desktop-bridge/src/computer-use.generated.ts b/packages/desktop-bridge/src/computer-use.generated.ts index 203e63b80d0..71f908ed036 100644 --- a/packages/desktop-bridge/src/computer-use.generated.ts +++ b/packages/desktop-bridge/src/computer-use.generated.ts @@ -17,6 +17,16 @@ const SnapshotId = z .describe("Latest unconsumed snapshotId from get_app_state for this app. Actions consume it."); const ElementId = z.string().min(1).max(128).describe("Element reference from that snapshot."); const WindowId = z.string().min(1).max(128).describe("Window reference from that snapshot."); +/** Input fields shared across actions keep one description: Anthropic flattens the union and anyOfs differing copies. */ +const TargetWindowId = WindowId.optional().describe( + "Window reference from an earlier snapshot of this app. If omitted, get_app_state and press_key use the app's focused window; coordinate actions use the window the snapshot selected.", +); +const IncludeScreenshot = z + .boolean() + .optional() + .describe( + "Set true to include a screenshot in the returned state. On actions it overrides observeAfter.includeScreenshot.", + ); const Coordinate = z.number().nonnegative().max(100_000); const ObserveAfter = { observeAfter: z @@ -25,14 +35,20 @@ const ObserveAfter = { .describe( "Every mutation returns fresh app state by default. Set includeScreenshot to true to include an image; otherwise no screenshot is captured. Observation failure never retries the action.", ), + includeScreenshot: IncludeScreenshot, }; const InputSequenceStep = z.discriminatedUnion("action", [ z.strictObject({ action: z.literal("type_text"), text: z.string().max(32_000) }), z.strictObject({ action: z.literal("press_key"), key: z.string().min(1).max(128) }), ]); const ElementTarget = { bundleId: BundleId, snapshotId: SnapshotId, elementId: ElementId }; -const WindowTarget = { bundleId: BundleId, snapshotId: SnapshotId, windowId: WindowId }; -const PointTarget = { ...WindowTarget, x: Coordinate, y: Coordinate }; +const PointTarget = { + bundleId: BundleId, + snapshotId: SnapshotId, + windowId: TargetWindowId, + x: Coordinate, + y: Coordinate, +}; const ClickOptions = { button: z.enum(["left", "right"]).optional(), clickCount: z.number().int().min(1).max(3).optional(), @@ -42,14 +58,23 @@ const ScrollOffsets = { .number() .min(-10_000) .max(10_000) - .describe("Horizontal scroll distance in points; positive scrolls right, negative left."), + .optional() + .describe( + "Horizontal scroll distance in points; positive scrolls right, negative left. Omit for 0 when deltaY is set.", + ), deltaY: z .number() .min(-10_000) .max(10_000) - .describe("Vertical scroll distance in points; positive scrolls down, negative up."), + .optional() + .describe( + "Vertical scroll distance in points; positive scrolls down, negative up. Omit for 0 when deltaX is set.", + ), }; +const hasScrollDistance = (input: { deltaX?: number | undefined; deltaY?: number | undefined }) => + input.deltaX !== undefined || input.deltaY !== undefined; + /** Strict branches require an observed, unambiguous target before native input. */ export const ComputerUseSchema = z.union([ z.strictObject({ action: z.literal("status") }), @@ -58,11 +83,8 @@ export const ComputerUseSchema = z.union([ z.strictObject({ action: z.literal("get_app_state"), bundleId: BundleId, - windowId: WindowId.optional(), - includeScreenshot: z - .boolean() - .optional() - .describe("Set true to include an image of the selected window."), + windowId: TargetWindowId, + includeScreenshot: IncludeScreenshot, }), z.strictObject({ action: z.literal("click"), ...ElementTarget, ...ObserveAfter, ...ClickOptions }), z.strictObject({ action: z.literal("click"), ...PointTarget, ...ObserveAfter, ...ClickOptions }), @@ -75,11 +97,24 @@ export const ComputerUseSchema = z.union([ z.strictObject({ action: z.literal("press_key"), ...ObserveAfter, - ...WindowTarget, - key: z.string().min(1).max(128).describe("One key or chord, such as Enter, Tab, Escape, or Cmd+A."), + bundleId: BundleId, + snapshotId: SnapshotId, + windowId: TargetWindowId, + elementId: ElementId.optional(), + key: z + .string() + .min(1) + .max(128) + .describe( + "One key or chord, such as Enter, Tab, Escape, or Cmd+A. With elementId, focuses that element first.", + ), }), - z.strictObject({ action: z.literal("scroll"), ...ElementTarget, ...ObserveAfter, ...ScrollOffsets }), - z.strictObject({ action: z.literal("scroll"), ...PointTarget, ...ObserveAfter, ...ScrollOffsets }), + z + .strictObject({ action: z.literal("scroll"), ...ElementTarget, ...ObserveAfter, ...ScrollOffsets }) + .refine(hasScrollDistance, "Scroll needs deltaX or deltaY."), + z + .strictObject({ action: z.literal("scroll"), ...PointTarget, ...ObserveAfter, ...ScrollOffsets }) + .refine(hasScrollDistance, "Scroll needs deltaX or deltaY."), z.strictObject({ action: z.literal("drag"), ...PointTarget, diff --git a/packages/testing/src/mocks/billing-usage-gate-cache.mock.ts b/packages/testing/src/mocks/billing-usage-gate-cache.mock.ts index 4142506127e..75ebe916263 100644 --- a/packages/testing/src/mocks/billing-usage-gate-cache.mock.ts +++ b/packages/testing/src/mocks/billing-usage-gate-cache.mock.ts @@ -5,7 +5,7 @@ import { vi } from 'vitest' * * The three `check*UsageLimits` gates are bare `vi.fn()` (resolve `undefined`); set * `mockResolvedValue({ isExceeded: false })` for an admitted path. `resetUsageGateCache` is a - * no-op. + * no-op, and `usageAdmissionTtlMs` returns the full gate TTL. * * @example * ```ts @@ -21,6 +21,8 @@ export const billingUsageGateCacheMockFns = { mockResetUsageGateCache: vi.fn((): void => {}), } +const USAGE_GATE_TTL_MS = 5 * 60 * 1000 + /** * Static mock module for `@/lib/billing/core/usage-gate-cache`. Constants carry the real * values (`USAGE_GATE_SETTLE_TIMEOUT_MS` = 2 x the 60 s ledger statement timeout + 15 s). @@ -31,10 +33,11 @@ export const billingUsageGateCacheMockFns = { * ``` */ export const billingUsageGateCacheMock = { - USAGE_GATE_TTL_MS: 5 * 60 * 1000, + USAGE_GATE_TTL_MS, USAGE_GATE_SETTLE_TIMEOUT_MS: 2 * 60_000 + 15_000, checkIngestionUsageLimits: billingUsageGateCacheMockFns.mockCheckIngestionUsageLimits, checkSearchUsageLimits: billingUsageGateCacheMockFns.mockCheckSearchUsageLimits, checkExecutionUsageLimits: billingUsageGateCacheMockFns.mockCheckExecutionUsageLimits, resetUsageGateCache: billingUsageGateCacheMockFns.mockResetUsageGateCache, + usageAdmissionTtlMs: vi.fn((): number => USAGE_GATE_TTL_MS), } diff --git a/packages/testing/src/mocks/billing-usage-log.mock.ts b/packages/testing/src/mocks/billing-usage-log.mock.ts index 9e1133828fa..ff7def0f720 100644 --- a/packages/testing/src/mocks/billing-usage-log.mock.ts +++ b/packages/testing/src/mocks/billing-usage-log.mock.ts @@ -39,6 +39,14 @@ export class MockCumulativeUsagePeriodClosedError extends Error { } } +/** Stand-in for `CumulativeUsageInFlightError` with the real `name`, `eventKey`, and message. */ +export class MockCumulativeUsageInFlightError extends Error { + constructor(readonly eventKey: string) { + super(`Cumulative usage event "${eventKey}" is already being recorded`) + this.name = 'CumulativeUsageInFlightError' + } +} + /** * Stand-in for `UnknownUsageCursorError` with the real `name`, message, and `statusCode` 400. * It is NOT a subclass of the real `HttpError`, and its `cause` is a plain `Error` carrying @@ -107,7 +115,8 @@ export const billingUsageLogMockFns = { /** * Static mock module for `@/lib/billing/core/usage-log`. Constants carry the real values; * `CumulativeUsageContextMismatchError` is {@link MockCumulativeUsageContextMismatchError}, - * `CumulativeUsagePeriodClosedError` is {@link MockCumulativeUsagePeriodClosedError}, and + * `CumulativeUsagePeriodClosedError` is {@link MockCumulativeUsagePeriodClosedError}, + * `CumulativeUsageInFlightError` is {@link MockCumulativeUsageInFlightError}, and * `UnknownUsageCursorError` is {@link MockUnknownUsageCursorError}. * * @example @@ -122,6 +131,7 @@ export const billingUsageLogMock = { UNKNOWN_CURSOR_MESSAGE, CumulativeUsageContextMismatchError: MockCumulativeUsageContextMismatchError, CumulativeUsagePeriodClosedError: MockCumulativeUsagePeriodClosedError, + CumulativeUsageInFlightError: MockCumulativeUsageInFlightError, UnknownUsageCursorError: MockUnknownUsageCursorError, isUnbilledUsageCategory: billingUsageLogMockFns.mockIsUnbilledUsageCategory, stableEventKey: billingUsageLogMockFns.mockStableEventKey, diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index 212acf24b63..f32c784e589 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -146,6 +146,7 @@ export { billingUsageLogMock, billingUsageLogMockFns, MockCumulativeUsageContextMismatchError, + MockCumulativeUsageInFlightError, MockCumulativeUsagePeriodClosedError, MockUnknownUsageCursorError, } from './billing-usage-log.mock' diff --git a/packages/testing/src/mocks/mothership-async-runs.mock.ts b/packages/testing/src/mocks/mothership-async-runs.mock.ts index a44ea88fd5c..bec5622e16e 100644 --- a/packages/testing/src/mocks/mothership-async-runs.mock.ts +++ b/packages/testing/src/mocks/mothership-async-runs.mock.ts @@ -7,7 +7,7 @@ import { vi } from 'vitest' * - `mockWithRunAdmissionLock` runs its `action` with an empty transaction object `{}` and returns * its result (the advisory lock itself is not modelled). * - `mockGetClaimedWorkflowExecutionId` is a faithful port (strips the real `'workflow:'` claim prefix). - * - `mockGetUnsettledStreamSandboxProcesses`, `mockGetUnsettledClientWorkflowExecutions` and + * - `mockStopLiveRunsTx`, `mockFindWorkspacesWithLiveRunsTx`, `mockGetUnsettledStreamSandboxProcesses`, `mockGetUnsettledClientWorkflowExecutions` and * `mockGetAsyncToolCalls` resolve `[]`. * * @example @@ -22,6 +22,8 @@ export const mothershipAsyncRunsMockFns = { async (_userId: string, _streamId: string, action: (tx: unknown) => unknown) => action({}) ), mockRequestRunStop: vi.fn(), + mockStopLiveRunsTx: vi.fn(async () => []), + mockFindWorkspacesWithLiveRunsTx: vi.fn(async () => []), mockIsRunStopRequested: vi.fn(), mockCreateRunSegment: vi.fn(), mockInsertRunSegment: vi.fn(), @@ -90,6 +92,8 @@ export const mothershipAsyncRunsMockFns = { export const mothershipAsyncRunsMock = { withRunAdmissionLock: mothershipAsyncRunsMockFns.mockWithRunAdmissionLock, requestRunStop: mothershipAsyncRunsMockFns.mockRequestRunStop, + stopLiveRunsTx: mothershipAsyncRunsMockFns.mockStopLiveRunsTx, + findWorkspacesWithLiveRunsTx: mothershipAsyncRunsMockFns.mockFindWorkspacesWithLiveRunsTx, isRunStopRequested: mothershipAsyncRunsMockFns.mockIsRunStopRequested, createRunSegment: mothershipAsyncRunsMockFns.mockCreateRunSegment, insertRunSegment: mothershipAsyncRunsMockFns.mockInsertRunSegment, diff --git a/packages/testing/src/mocks/mothership-chat-lifecycle.mock.ts b/packages/testing/src/mocks/mothership-chat-lifecycle.mock.ts index ff2a418b067..3770c2daf72 100644 --- a/packages/testing/src/mocks/mothership-chat-lifecycle.mock.ts +++ b/packages/testing/src/mocks/mothership-chat-lifecycle.mock.ts @@ -17,9 +17,10 @@ export const mothershipChatLifecycleMockFns = { mockLoadChatFileAttachmentsNamed: vi.fn(), mockGetAccessibleCopilotChatAuth: vi.fn(), mockGetAccessibleCopilotChatForCancellation: vi.fn(), - mockGetAccessibleCopilotChat: vi.fn(), mockGetAccessibleCopilotChatWithMessages: vi.fn(), mockResolveOrCreateChat: vi.fn(), + mockAuthorizeChatWorkflowRead: vi.fn(), + mockIsChatModeAvailable: vi.fn(async () => true), } /** @@ -37,8 +38,9 @@ export const mothershipChatLifecycleMock = { getAccessibleCopilotChatAuth: mothershipChatLifecycleMockFns.mockGetAccessibleCopilotChatAuth, getAccessibleCopilotChatForCancellation: mothershipChatLifecycleMockFns.mockGetAccessibleCopilotChatForCancellation, - getAccessibleCopilotChat: mothershipChatLifecycleMockFns.mockGetAccessibleCopilotChat, getAccessibleCopilotChatWithMessages: mothershipChatLifecycleMockFns.mockGetAccessibleCopilotChatWithMessages, resolveOrCreateChat: mothershipChatLifecycleMockFns.mockResolveOrCreateChat, + authorizeChatWorkflowRead: mothershipChatLifecycleMockFns.mockAuthorizeChatWorkflowRead, + isChatModeAvailable: mothershipChatLifecycleMockFns.mockIsChatModeAvailable, } diff --git a/packages/testing/src/mocks/organization-authorization.mock.ts b/packages/testing/src/mocks/organization-authorization.mock.ts index 0e39b03c8e1..5cd800d7714 100644 --- a/packages/testing/src/mocks/organization-authorization.mock.ts +++ b/packages/testing/src/mocks/organization-authorization.mock.ts @@ -15,6 +15,19 @@ export class MockOrganizationMembershipNotFoundError extends Error { } } +/** + * Mirrors `OrganizationDelegationAuthorizationError`: same `name` (`OrchestrationError`), `code` + * (`forbidden`) and message, extending plain `Error` like the class above. + */ +class MockOrganizationDelegationAuthorizationError extends Error { + readonly code = 'forbidden' as const + + constructor() { + super('Organization delegation is no longer valid') + this.name = 'OrchestrationError' + } +} + /** * Controllable mock functions for `@/lib/core/application/organization-authorization`. * Both are bare `vi.fn()`s (resolve `undefined`); set the membership context per test. @@ -43,6 +56,7 @@ export const organizationAuthorizationMockFns = { */ export const organizationAuthorizationMock = { OrganizationMembershipNotFoundError: MockOrganizationMembershipNotFoundError, + OrganizationDelegationAuthorizationError: MockOrganizationDelegationAuthorizationError, requireOrganizationMembership: organizationAuthorizationMockFns.mockRequireOrganizationMembership, authorizeOrganizationOperation: organizationAuthorizationMockFns.mockAuthorizeOrganizationOperation, diff --git a/packages/testing/src/mocks/schema-tables.generated.ts b/packages/testing/src/mocks/schema-tables.generated.ts index 869c97f393b..767c5521523 100644 --- a/packages/testing/src/mocks/schema-tables.generated.ts +++ b/packages/testing/src/mocks/schema-tables.generated.ts @@ -1207,6 +1207,7 @@ export const GENERATED_SCHEMA_TABLES = { 'executionLeaseExpiresAt', 'executionRevokedAt', 'executionAttempts', + 'executionDeadlineAt', 'clientWorkflowExecutionId', 'sandboxProcesses', 'completedAt', diff --git a/scripts/check-explicit-any.baseline.json b/scripts/check-explicit-any.baseline.json index 1f3e57a79e0..ca15838f317 100644 --- a/scripts/check-explicit-any.baseline.json +++ b/scripts/check-explicit-any.baseline.json @@ -206,7 +206,7 @@ "apps/sim/lib/billing/client/upgrade.ts": 3, "apps/sim/lib/billing/core/subscription.ts": 1, "apps/sim/lib/billing/subscriptions/utils.ts": 7, - "apps/sim/lib/content/mdx.tsx": 15, + "apps/sim/lib/content/mdx.tsx": 14, "apps/sim/lib/content/registry-factory.ts": 3, "apps/sim/lib/core/config/redis.ts": 1, "apps/sim/lib/core/idempotency/service.ts": 6, @@ -1115,7 +1115,6 @@ "apps/sim/app/api/billing/invoices/route.ts": 2, "apps/sim/app/api/billing/route.ts": 1, "apps/sim/app/api/chat/[identifier]/route.ts": 1, - "apps/sim/app/api/copilot/chat/queries.ts": 1, "apps/sim/app/api/copilot/chat/stream/route.test.ts": 1, "apps/sim/app/api/copilot/chat/stream/route.ts": 1, "apps/sim/app/api/copilot/chats/route.ts": 2, @@ -1475,7 +1474,6 @@ "apps/sim/lib/knowledge/__integration__/knowledge-projection.integration.ts": 3, "apps/sim/lib/knowledge/__integration__/listing-continuation.integration.ts": 22, "apps/sim/lib/knowledge/__integration__/ocr-input-failures.integration.ts": 2, - "apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts": 2, "apps/sim/lib/knowledge/__integration__/provider-admission.integration.ts": 1, "apps/sim/lib/knowledge/__integration__/provider-processing-recovery.integration.ts": 3, "apps/sim/lib/knowledge/__integration__/providers-live.integration.ts": 10, @@ -1574,7 +1572,6 @@ "apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts": 10, "apps/sim/lib/mothership/auth/internal.ts": 1, "apps/sim/lib/mothership/billing/service-store.integration.ts": 3, - "apps/sim/lib/mothership/chat/application/context.ts": 1, "apps/sim/lib/mothership/chat/application/fork.ts": 2, "apps/sim/lib/mothership/chat/application/inline-images.ts": 1, "apps/sim/lib/mothership/chat/application/use-cases.test.ts": 1, diff --git a/scripts/check-source-conventions.baseline.json b/scripts/check-source-conventions.baseline.json index ecc1d0dc95d..83492b0745a 100644 --- a/scripts/check-source-conventions.baseline.json +++ b/scripts/check-source-conventions.baseline.json @@ -165,9 +165,9 @@ "apps/sim/lib/core/async-jobs/types.test.ts": 1, "apps/sim/lib/core/config/cost-multiplier.ts": 1, "apps/sim/lib/core/config/env-flags.ts": 3, + "apps/sim/lib/core/rate-limiter/hosted-key/queue.test.ts": 1, "apps/sim/lib/core/rate-limiter/hosted-key/rate-limiter.test.ts": 3, "apps/sim/lib/core/rate-limiter/hosted-key/rate-limiter.ts": 2, - "apps/sim/lib/core/rate-limiter/hosted-key/queue.test.ts": 1, "apps/sim/lib/core/rate-limiter/rate-limiter.test.ts": 2, "apps/sim/lib/core/rate-limiter/rate-limiter.ts": 2, "apps/sim/lib/core/rate-limiter/route-helpers.test.ts": 1, @@ -207,11 +207,6 @@ "apps/sim/lib/mothership/chat/selection-context.test.ts": 1, "apps/sim/lib/mothership/chat/sim-key-redaction.test.ts": 1, "apps/sim/lib/mothership/chat/terminal-state.test.ts": 1, - "apps/sim/lib/mothership/generated/agent-cli.ts": 2, - "apps/sim/lib/mothership/generated/assistant.ts": 1, - "apps/sim/lib/mothership/generated/protocol.ts": 3, - "apps/sim/lib/mothership/generated/resources.ts": 1, - "apps/sim/lib/mothership/generated/sim-transport.ts": 4, "apps/sim/lib/mothership/request/go/inline-images.test.ts": 1, "apps/sim/lib/mothership/request/handlers/complete.ts": 2, "apps/sim/lib/mothership/request/handlers/error.ts": 2, @@ -280,9 +275,9 @@ "apps/sim/lib/pptx-renderer/shapes/presets.ts": 1, "apps/sim/lib/table/workflow-columns.ts": 1, "apps/sim/lib/tools/falai-pricing.test.ts": 1, - "apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts": 1, "apps/sim/lib/uploads/contexts/workspace/file-manager-errors.test.ts": 1, "apps/sim/lib/uploads/contexts/workspace/file-manager.test.ts": 1, + "apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts": 1, "apps/sim/lib/webhooks/providers/table.ts": 1, "apps/sim/lib/webhooks/providers/whatsapp.test.ts": 1, "apps/sim/lib/workflows/comparison/normalize.test.ts": 1, @@ -593,8 +588,6 @@ "apps/sim/app/api/chat/utils.ts": 2, "apps/sim/app/api/chat/validate/route.ts": 2, "apps/sim/app/api/copilot/api-keys/validate/route.ts": 2, - "apps/sim/app/api/copilot/chat/delete/route.ts": 2, - "apps/sim/app/api/copilot/chat/queries.ts": 2, "apps/sim/app/api/copilot/chats/route.ts": 2, "apps/sim/app/api/copilot/feedback/route.ts": 2, "apps/sim/app/api/cron/reconcile-inbox-entitlement/route.ts": 1, diff --git a/scripts/check-source-conventions.test.ts b/scripts/check-source-conventions.test.ts index 205c04f2131..c832ca7b418 100644 --- a/scripts/check-source-conventions.test.ts +++ b/scripts/check-source-conventions.test.ts @@ -103,6 +103,26 @@ describe('relative-import', () => { hits('packages/utils/src/object.ts', "import { a } from './a'", 'relative-import') ).toEqual([]) }) + + it('exempts a Mothership contract copy only where contracts:sync writes it', () => { + const copy = [ + '// GENERATED — do not edit. Source of truth: mothership worker packages/contracts/src/sim-transport.ts', + "import { MemoryNotesAccessInput } from './memory-notes-access'", + ].join('\n') + expect( + hits('apps/sim/lib/mothership/generated/sim-transport.ts', copy, 'relative-import') + ).toEqual([]) + expect(hits('apps/sim/lib/feature/service.ts', copy, 'relative-import')).toEqual([ + './memory-notes-access', + ]) + expect( + hits( + 'apps/sim/lib/mothership/generated/handwritten.ts', + "import { a } from './a'", + 'relative-import' + ) + ).toEqual(['./a']) + }) }) describe('text-size-literal', () => { diff --git a/scripts/check-source-conventions.ts b/scripts/check-source-conventions.ts index e31a065e8b5..0f7ad113d9d 100644 --- a/scripts/check-source-conventions.ts +++ b/scripts/check-source-conventions.ts @@ -9,6 +9,8 @@ * import whose every binding the barrel then exports (`export { X }`, or renamed with * `export const Y = X`). A path that leaves the folder (`../x`, * `./../x`), a side-effect import, and an import the barrel keeps for itself are all flagged. + * A verbatim Mothership contract copy in `lib/mothership/generated/` is exempt: its imports mirror + * the worker source it is synced from, so it cannot use the app's aliases. * - `text-size-literal` (`.claude/rules/sim-styling.md` "Tailwind" 3): an `apps/sim` string or * template literal sets type size with `text-[Npx]` where the documented text scale has a token * for that size (`text-[13px]` → `text-small`). The literal also drops the token's line-height. @@ -522,6 +524,15 @@ function findBareRouteHandlers(file: string, program: Node): Violation[] { return violations } +const CONTRACT_COPY_DIR = 'apps/sim/lib/mothership/generated/' +const CONTRACT_COPY_HEADER = + '// GENERATED — do not edit. Source of truth: mothership worker packages/contracts/' + +/** A file `contracts:sync` copies byte for byte from the Mothership worker. */ +function isContractCopy(file: string, source: string): boolean { + return file.startsWith(CONTRACT_COPY_DIR) && source.startsWith(CONTRACT_COPY_HEADER) +} + /** Unit, integration and live tests, plus fixtures: they bind real boundaries on purpose. */ function isTestFile(file: string): boolean { return /\.(?:test|spec|integration)\.tsx?$/.test(file) || file.includes('/__fixtures__/') @@ -533,7 +544,7 @@ export function findViolations(file: string, source: string): Violation[] { const test = isTestFile(file) const isApi = file.startsWith('apps/sim/app/api/') && !test // Cheap textual gates; each is a superset of what its rule can match, and the AST decides. - const wantsRelative = /['"`]\.\.?\//.test(source) + const wantsRelative = !isContractCopy(file, source) && /['"`]\.\.?\//.test(source) const wantsTextSize = !test && /text-\[\d/.test(source) const wantsStyle = !test && file.endsWith('.tsx') && /\bstyle\s*=\s*\{/.test(source) const wantsDb = isApi && source.includes('@sim/db') diff --git a/scripts/check-unused-exports.baseline.json b/scripts/check-unused-exports.baseline.json index 032ed628d74..0dcd68ae086 100644 --- a/scripts/check-unused-exports.baseline.json +++ b/scripts/check-unused-exports.baseline.json @@ -1310,7 +1310,6 @@ "apps/sim/lib/api/contracts/copilot.ts#ValidateCopilotByokBody", "apps/sim/lib/api/contracts/copilot.ts#copilotApiKeySchema", "apps/sim/lib/api/contracts/copilot.ts#copilotChatAbortBodySchema", - "apps/sim/lib/api/contracts/copilot.ts#copilotChatGetQuerySchema", "apps/sim/lib/api/contracts/copilot.ts#copilotChatListItemSchema", "apps/sim/lib/api/contracts/copilot.ts#copilotChatSteerBodySchema", "apps/sim/lib/api/contracts/copilot.ts#copilotChatStreamQuerySchema", @@ -4185,7 +4184,6 @@ "apps/sim/lib/content/seo.ts#buildBreadcrumbJsonLd", "apps/sim/lib/core/application/index.ts#AuthorizedWorkspaceResourceUseCaseContext", "apps/sim/lib/core/application/index.ts#AuthorizedWorkspaceUseCaseResultContext", - "apps/sim/lib/core/application/index.ts#DelegatedServiceAuthorizationError", "apps/sim/lib/core/application/index.ts#FORBIDDEN_DETAIL_CODES", "apps/sim/lib/core/application/index.ts#OAuthOperationPolicy", "apps/sim/lib/core/application/index.ts#OperationDeclarableCapability", @@ -4197,7 +4195,6 @@ "apps/sim/lib/core/application/index.ts#requirePersonalApiKeysAllowed", "apps/sim/lib/core/application/operation.ts#PrincipalWideCapability", "apps/sim/lib/core/application/organization-operation.ts#OrganizationPrincipal", - "apps/sim/lib/core/application/workspace-authorization.ts#DelegatedServiceAuthorizationError", "apps/sim/lib/core/application/workspace-authorization.ts#requireCliAccessAllowed", "apps/sim/lib/core/application/workspace-authorization.ts#requirePersonalApiKeysAllowed", "apps/sim/lib/core/async-jobs/config.ts#getAsyncBackendType", @@ -5080,7 +5077,6 @@ "apps/sim/lib/uploads/utils/validation.ts#SupportedVideoExtension", "apps/sim/lib/uploads/utils/validation.ts#isValidPng", "apps/sim/lib/url-state/index.ts#SortDefault", - "apps/sim/lib/users/account-deletion.ts#AccountDeletionBlockedError", "apps/sim/lib/webhooks/deployed-urls.ts#resolveDeployedWebhookUrl", "apps/sim/lib/webhooks/env-resolver.ts#normalizeWebhookProviderConfig", "apps/sim/lib/webhooks/execution-principal.ts#normalizeWebhookPrincipalProvider", diff --git a/scripts/test-patterns-baseline.json b/scripts/test-patterns-baseline.json index 30969c0dc8f..0d7ed8685bb 100644 --- a/scripts/test-patterns-baseline.json +++ b/scripts/test-patterns-baseline.json @@ -151,7 +151,6 @@ "real-wait\tapps/sim/hooks/queries/workspace.test.tsx\tsleep() #1", "real-wait\tapps/sim/lib/billing/core/reporting-usage-cache.test.ts\tsleep() #1", "real-wait\tapps/sim/lib/billing/core/reporting-usage-cache.test.ts\tsleep() #2", - "real-wait\tapps/sim/lib/billing/core/usage-gate-cache.test.ts\tsleep() #1", "real-wait\tapps/sim/lib/concurrency/leader-lock.test.ts\tsleep() #1", "real-wait\tapps/sim/lib/concurrency/singleflight.test.ts\tsleep() #1", "real-wait\tapps/sim/lib/core/async-jobs/backends/database.test.ts\tsleep() #1",