diff --git a/agents/base2/base2.ts b/agents/base2/base2.ts index 662cc2a775..077dcd78f6 100644 --- a/agents/base2/base2.ts +++ b/agents/base2/base2.ts @@ -1,4 +1,5 @@ import { buildArray } from '@codebuff/common/util/array' +import { COMPOSIO_META_TOOL_NAMES } from '@codebuff/common/constants/composio' import { FREEBUFF_GEMINI_THINKER_AGENT_ID, FREEBUFF_GEMINI_THINKER_INSTRUCTIONS_PROMPT, @@ -17,6 +18,8 @@ import { type SecretAgentDefinition, } from '../types/secret-agent-definition' +const ENABLE_COMPOSIO_TOOLS = false + export function createBase2( mode: 'default' | 'free' | 'lite' | 'max' | 'fast', options?: { @@ -105,6 +108,7 @@ export function createBase2( 'set_output', 'list_directory', 'glob', + ENABLE_COMPOSIO_TOOLS && COMPOSIO_META_TOOL_NAMES, ), spawnableAgents: buildArray( !isMax && 'file-picker', @@ -148,7 +152,8 @@ Current date: ${PLACEHOLDER.CURRENT_DATE}. } - **Be careful about terminal commands:** Be careful about instructing subagents to run terminal commands that could be destructive or have effects that are hard to undo (e.g. git push, git commit, running any scripts -- especially ones that could alter production environments (!), installing packages globally, etc). Don't run any of these effectful commands unless the user explicitly asks you to. - **Do what the user asks:** If the user asks you to do something, even running a risky terminal command, do it. -- **Don't use set_output:** The set_output tool is for spawned subagents to report results. Don't use it yourself. +- **Don't use set_output:** The set_output tool is for spawned subagents to report results. Don't use it yourself.${ENABLE_COMPOSIO_TOOLS ? ` +- **External apps:** When Composio tools are available and the user asks to work with connected apps or services like Gmail, Google Calendar, GitHub, Slack, Linear, or Notion, use them to search for the right app tools, help the user connect their account, and execute the requested action.` : ''} # Code Editing Mandates diff --git a/agents/types/secret-agent-definition.ts b/agents/types/secret-agent-definition.ts index cab28c2669..6718cea418 100644 --- a/agents/types/secret-agent-definition.ts +++ b/agents/types/secret-agent-definition.ts @@ -1,5 +1,6 @@ import type { AgentDefinition } from './agent-definition' import type * as Tools from './tools' +import type { ComposioMetaToolName } from '@codebuff/common/constants/composio' export type { Tools } export type AllToolNames = @@ -9,9 +10,12 @@ export type AllToolNames = | 'create_plan' | 'spawn_agent_inline' | 'update_subgoal' + | ComposioMetaToolName -export interface SecretAgentDefinition - extends Omit { +export interface SecretAgentDefinition extends Omit< + AgentDefinition, + 'toolNames' +> { /** Tools this agent can use. */ toolNames?: AllToolNames[] } diff --git a/bun.lock b/bun.lock index 4f6021307f..8868dc26ae 100644 --- a/bun.lock +++ b/bun.lock @@ -272,6 +272,7 @@ "@codebuff/common": "workspace:*", "@codebuff/internal": "workspace:*", "@codebuff/sdk": "workspace:*", + "@composio/core": "^0.10.0", "@hookform/resolvers": "^3.9.0", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", @@ -550,6 +551,12 @@ "@commitlint/types": ["@commitlint/types@19.8.1", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw=="], + "@composio/client": ["@composio/client@0.1.0-alpha.72", "", {}, "sha512-2WYXgdlMvhoaJCsaSfyMAYGQ2tS5l2Fqdh2cMRqNi8NybIoOkFZy2ApBJJOoQwqfpUr41VymMW6FtiNQm7JavQ=="], + + "@composio/core": ["@composio/core@0.10.0", "", { "dependencies": { "@composio/client": "0.1.0-alpha.72", "@composio/json-schema-to-zod": "0.1.20", "@types/json-schema": "^7.0.15", "chalk": "^4.1.2", "openai": "^6.16.0", "pusher-js": "^8.4.0", "semver": "^7.7.2", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-dG2BKF4NRiE8HHDzWLLgjMMo3FU4NJ6SxR961EpdLWWEKhkl+yP/mZlIN7p/4WnCR/fuDBKH1Qh+7kBdcHmEHA=="], + + "@composio/json-schema-to-zod": ["@composio/json-schema-to-zod@0.1.20", "", { "peerDependencies": { "zod": ">=3.25.76 <5" } }, "sha512-d4V34itLrUWG/VBh7ciznKcxF/T22MBLHmuEzHoX0zsBOHsUmjYz5qtDh20S2p3FE+HHvLZxpXiv8yfdd4yI+Q=="], + "@contentlayer2/cli": ["@contentlayer2/cli@0.5.8", "", { "dependencies": { "@contentlayer2/core": "0.5.8", "@contentlayer2/utils": "0.5.8", "clipanion": "^3.2.1", "typanion": "^3.12.1" } }, "sha512-sPXTe24tXPpru6hE45riBj7xjVIDuTfjQXbwitwcNkm0yd0kNJaDPBA2C4U5mRFgg1a/aftlIKeVavlkBnuZQA=="], "@contentlayer2/client": ["@contentlayer2/client@0.5.8", "", { "dependencies": { "@contentlayer2/core": "0.5.8" } }, "sha512-mc6uGuHI5ygO6s5KHhoFfiUN7BUUHrPUsxjU+EnGHjMo3+rcYnHd+G6cDDcf0fB21cX7NbJ57V9UlAMu5JKM0w=="], @@ -2900,6 +2907,8 @@ "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "openai": ["openai@6.39.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ=="], + "openid-client": ["openid-client@5.7.1", "", { "dependencies": { "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -3094,6 +3103,8 @@ "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "pusher-js": ["pusher-js@8.5.0", "", { "dependencies": { "tweetnacl": "^1.0.3" } }, "sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw=="], + "qs": ["qs@6.11.0", "", { "dependencies": { "side-channel": "^1.0.4" } }, "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -3470,6 +3481,8 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + "typanion": ["typanion@3.14.0", "", {}, "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -3698,6 +3711,10 @@ "@commitlint/top-level/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], + "@composio/core/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "@composio/core/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@contentlayer2/utils/@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="], "@contentlayer2/utils/@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="], @@ -4310,6 +4327,8 @@ "@commitlint/top-level/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + "@composio/core/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "@contentlayer2/utils/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], "@contentlayer2/utils/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], diff --git a/common/src/constants/composio.ts b/common/src/constants/composio.ts new file mode 100644 index 0000000000..c98c746252 --- /dev/null +++ b/common/src/constants/composio.ts @@ -0,0 +1,34 @@ +export const COMPOSIO_API_KEY_ENV_VAR = 'COMPOSIO_API_KEY' + +export const COMPOSIO_META_TOOL_NAMES = [ + 'composio_manage_connections', + 'composio_multi_execute_tool', + 'composio_search_tools', + 'composio_get_tool_schemas', +] as const + +export type ComposioMetaToolName = (typeof COMPOSIO_META_TOOL_NAMES)[number] + +export const COMPOSIO_META_TOOL_NAME_TO_UPSTREAM = { + composio_manage_connections: 'COMPOSIO_MANAGE_CONNECTIONS', + composio_multi_execute_tool: 'COMPOSIO_MULTI_EXECUTE_TOOL', + composio_search_tools: 'COMPOSIO_SEARCH_TOOLS', + composio_get_tool_schemas: 'COMPOSIO_GET_TOOL_SCHEMAS', +} as const satisfies Record + +export type ComposioUpstreamMetaToolName = + (typeof COMPOSIO_META_TOOL_NAME_TO_UPSTREAM)[ComposioMetaToolName] + +const COMPOSIO_META_TOOL_NAME_SET = new Set(COMPOSIO_META_TOOL_NAMES) + +export function isComposioMetaToolName( + toolName: string, +): toolName is ComposioMetaToolName { + return COMPOSIO_META_TOOL_NAME_SET.has(toolName) +} + +export function getComposioUpstreamToolName( + toolName: ComposioMetaToolName, +): ComposioUpstreamMetaToolName { + return COMPOSIO_META_TOOL_NAME_TO_UPSTREAM[toolName] +} diff --git a/common/src/tools/constants.ts b/common/src/tools/constants.ts index 5fe789eb76..89c89f7038 100644 --- a/common/src/tools/constants.ts +++ b/common/src/tools/constants.ts @@ -1,3 +1,5 @@ +import { COMPOSIO_META_TOOL_NAMES } from '../constants/composio' + import type { ToolResultOutput } from '../types/messages/content-part' import type { Tool } from 'ai' @@ -56,6 +58,7 @@ export const toolNames = [ 'web_search', 'write_file', 'write_todos', + ...COMPOSIO_META_TOOL_NAMES, ] as const export const publishedTools = [ diff --git a/common/src/tools/list.ts b/common/src/tools/list.ts index 4f40570d0e..fbf59f3682 100644 --- a/common/src/tools/list.ts +++ b/common/src/tools/list.ts @@ -7,6 +7,7 @@ import { applyPatchParams } from './params/tool/apply-patch' import { askUserParams } from './params/tool/ask-user' import { browserLogsParams } from './params/tool/browser-logs' import { codeSearchParams } from './params/tool/code-search' +import { composioMetaToolParams } from './params/tool/composio' import { createPlanParams } from './params/tool/create-plan' import { endTurnParams } from './params/tool/end-turn' import { findFilesParams } from './params/tool/find-files' @@ -77,6 +78,7 @@ export const toolParams = { web_search: webSearchParams, write_file: writeFileParams, write_todos: writeTodosParams, + ...composioMetaToolParams, } satisfies { [K in ToolName]: $ToolParams } @@ -151,6 +153,22 @@ export const clientToolCallSchema = z.discriminatedUnion('toolName', [ toolName: z.literal('write_file'), input: FileChangeSchema, }), + z.object({ + toolName: z.literal('composio_manage_connections'), + input: toolParams.composio_manage_connections.inputSchema, + }), + z.object({ + toolName: z.literal('composio_multi_execute_tool'), + input: toolParams.composio_multi_execute_tool.inputSchema, + }), + z.object({ + toolName: z.literal('composio_search_tools'), + input: toolParams.composio_search_tools.inputSchema, + }), + z.object({ + toolName: z.literal('composio_get_tool_schemas'), + input: toolParams.composio_get_tool_schemas.inputSchema, + }), ]) export const clientToolNames = clientToolCallSchema.def.options.map( (opt) => opt.shape.toolName.value, @@ -163,4 +181,4 @@ export type ClientToolCall = Extract< > & Pick -export type PublishedClientToolName = ClientToolName & PublishedToolName +export type PublishedClientToolName = Extract diff --git a/common/src/tools/params/tool/composio.ts b/common/src/tools/params/tool/composio.ts new file mode 100644 index 0000000000..a990d767f2 --- /dev/null +++ b/common/src/tools/params/tool/composio.ts @@ -0,0 +1,131 @@ +import { COMPOSIO_META_TOOL_NAMES } from '../../../constants/composio' +import z from 'zod/v4' + +import { jsonToolResultSchema } from '../utils' + +import type { $ToolParams } from '../../constants' + +const sessionIdParam = z + .string() + .optional() + .describe('Session ID returned by composio_search_tools, when available.') + +const composioMetaToolInputSchemas = { + composio_search_tools: z + .object({ + queries: z + .array(z.unknown()) + .min(1) + .describe( + 'Structured English search queries. Split independent app/API actions into separate queries.', + ), + session: z + .object({ + generate_id: z.boolean().optional(), + id: z.string().optional(), + }) + .catchall(z.unknown()) + .describe( + 'Use { generate_id: true } for a new workflow, or { id } to continue one.', + ), + model: z.string().optional().describe('Client LLM model name.'), + }) + .catchall(z.unknown()), + composio_get_tool_schemas: z + .object({ + tool_slugs: z + .array(z.string()) + .min(1) + .describe('Composio tool slugs to retrieve schemas for.'), + include: z + .array(z.string()) + .optional() + .describe('Schema fields to include, e.g. input_schema/output_schema.'), + session_id: sessionIdParam, + }) + .catchall(z.unknown()), + composio_manage_connections: z + .object({ + toolkits: z + .array(z.string()) + .min(1) + .describe('Toolkit slugs to check or connect, such as gmail/github.'), + reinitiate_all: z + .boolean() + .optional() + .describe('Force reconnection even if active credentials exist.'), + session_id: sessionIdParam, + }) + .catchall(z.unknown()), + composio_multi_execute_tool: z + .object({ + tools: z + .array(z.record(z.string(), z.unknown())) + .min(1) + .describe('Logically independent Composio tools to execute.'), + thought: z + .string() + .optional() + .describe('One concise sentence explaining the execution intent.'), + sync_response_to_workbench: z + .boolean() + .default(false) + .describe('Always use false. Codebuff disables Composio workbench.'), + session_id: sessionIdParam, + }) + .catchall(z.unknown()), +} + +const composioMetaToolDescriptions = { + composio_search_tools: + 'Discover relevant Composio tools across external apps. Use this first for requests involving services like Gmail, GitHub, Slack, Linear, Notion, Google Calendar, or Google Sheets.', + composio_get_tool_schemas: + 'Retrieve complete input schemas for specific Composio tool slugs returned by composio_search_tools.', + composio_manage_connections: + 'Check or initiate user authentication for external app toolkits. Use when search/execution indicates a toolkit is not connected.', + composio_multi_execute_tool: + 'Execute one or more discovered Composio app tools in the current workflow session. Do not use workbench offloading.', +} + +const composioOutputSchema = jsonToolResultSchema( + z.union([ + z.json(), + z.object({ + errorMessage: z.string(), + status: z.number().optional(), + }), + ]), +) + +export const composioMetaToolParams = { + composio_manage_connections: { + toolName: 'composio_manage_connections', + endsAgentStep: true, + description: composioMetaToolDescriptions.composio_manage_connections, + inputSchema: composioMetaToolInputSchemas.composio_manage_connections, + outputSchema: composioOutputSchema, + }, + composio_multi_execute_tool: { + toolName: 'composio_multi_execute_tool', + endsAgentStep: true, + description: composioMetaToolDescriptions.composio_multi_execute_tool, + inputSchema: composioMetaToolInputSchemas.composio_multi_execute_tool, + outputSchema: composioOutputSchema, + }, + composio_search_tools: { + toolName: 'composio_search_tools', + endsAgentStep: true, + description: composioMetaToolDescriptions.composio_search_tools, + inputSchema: composioMetaToolInputSchemas.composio_search_tools, + outputSchema: composioOutputSchema, + }, + composio_get_tool_schemas: { + toolName: 'composio_get_tool_schemas', + endsAgentStep: true, + description: composioMetaToolDescriptions.composio_get_tool_schemas, + inputSchema: composioMetaToolInputSchemas.composio_get_tool_schemas, + outputSchema: composioOutputSchema, + }, +} satisfies { + [K in (typeof COMPOSIO_META_TOOL_NAMES)[number]]: $ToolParams +} diff --git a/packages/agent-runtime/src/tools/handlers/list.ts b/packages/agent-runtime/src/tools/handlers/list.ts index abb7c340db..4d2c6ea836 100644 --- a/packages/agent-runtime/src/tools/handlers/list.ts +++ b/packages/agent-runtime/src/tools/handlers/list.ts @@ -4,6 +4,12 @@ import { handleApplyPatch } from './tool/apply-patch' import { handleAskUser } from './tool/ask-user' import { handleBrowserLogs } from './tool/browser-logs' import { handleCodeSearch } from './tool/code-search' +import { + handleComposioGetToolSchemas, + handleComposioManageConnections, + handleComposioMultiExecute, + handleComposioSearchTools, +} from './tool/composio' import { handleCreatePlan } from './tool/create-plan' import { handleEndTurn } from './tool/end-turn' import { handleFindFiles } from './tool/find-files' @@ -46,13 +52,19 @@ import type { ToolName } from '@codebuff/common/tools/constants' * - Any additional arguments for the tool * - Returns a promise that will be awaited */ -export const codebuffToolHandlers = { +export const codebuffToolHandlers: { + [K in ToolName]: CodebuffToolHandlerFunction +} = { add_message: handleAddMessage, add_subgoal: handleAddSubgoal, apply_patch: handleApplyPatch, ask_user: handleAskUser, browser_logs: handleBrowserLogs, code_search: handleCodeSearch, + composio_manage_connections: handleComposioManageConnections, + composio_multi_execute_tool: handleComposioMultiExecute, + composio_search_tools: handleComposioSearchTools, + composio_get_tool_schemas: handleComposioGetToolSchemas, create_plan: handleCreatePlan, end_turn: handleEndTurn, find_files: handleFindFiles, @@ -82,6 +94,4 @@ export const codebuffToolHandlers = { web_search: handleWebSearch, write_file: handleWriteFile, write_todos: handleWriteTodos, -} satisfies { - [K in ToolName]: CodebuffToolHandlerFunction } diff --git a/packages/agent-runtime/src/tools/handlers/tool/composio.ts b/packages/agent-runtime/src/tools/handlers/tool/composio.ts new file mode 100644 index 0000000000..70f7ddcba5 --- /dev/null +++ b/packages/agent-runtime/src/tools/handlers/tool/composio.ts @@ -0,0 +1,37 @@ +import type { ComposioMetaToolName } from '@codebuff/common/constants/composio' +import type { CodebuffToolOutput } from '@codebuff/common/tools/list' +import type { CodebuffToolHandlerFunction } from '../handler-function-type' + +function makeComposioHandler< + T extends ComposioMetaToolName, +>(): CodebuffToolHandlerFunction { + return async ({ toolCall, requestClientToolCall }) => { + if (!requestClientToolCall) { + return { + output: [ + { + type: 'json', + value: { + errorMessage: 'Composio tools are not available in this runtime.', + }, + }, + ], + } + } + + return { + output: (await (requestClientToolCall as any)( + toolCall, + )) as CodebuffToolOutput, + } + } +} + +export const handleComposioManageConnections: CodebuffToolHandlerFunction<'composio_manage_connections'> = + makeComposioHandler<'composio_manage_connections'>() +export const handleComposioMultiExecute: CodebuffToolHandlerFunction<'composio_multi_execute_tool'> = + makeComposioHandler<'composio_multi_execute_tool'>() +export const handleComposioSearchTools: CodebuffToolHandlerFunction<'composio_search_tools'> = + makeComposioHandler<'composio_search_tools'>() +export const handleComposioGetToolSchemas: CodebuffToolHandlerFunction<'composio_get_tool_schemas'> = + makeComposioHandler<'composio_get_tool_schemas'>() diff --git a/packages/internal/src/db/migrations/0056_simple_tyrannus.sql b/packages/internal/src/db/migrations/0056_simple_tyrannus.sql new file mode 100644 index 0000000000..e63baa377e --- /dev/null +++ b/packages/internal/src/db/migrations/0056_simple_tyrannus.sql @@ -0,0 +1,9 @@ +CREATE TABLE "composio_session" ( + "user_id" text PRIMARY KEY NOT NULL, + "session_id" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "composio_session_session_id_unique" UNIQUE("session_id") +); +--> statement-breakpoint +ALTER TABLE "composio_session" ADD CONSTRAINT "composio_session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; diff --git a/packages/internal/src/db/migrations/meta/0056_snapshot.json b/packages/internal/src/db/migrations/meta/0056_snapshot.json new file mode 100644 index 0000000000..08e5837ae0 --- /dev/null +++ b/packages/internal/src/db/migrations/meta/0056_snapshot.json @@ -0,0 +1,3649 @@ +{ + "id": "d047421b-9f49-48d9-89db-fbd81b41ab64", + "prevId": "fffbacec-ce15-4625-9adb-5c5a45bb9c3e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ad_impression": { + "name": "ad_impression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'gravity'" + }, + "ad_text": { + "name": "ad_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta": { + "name": "cta", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_url": { + "name": "click_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imp_url": { + "name": "imp_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extra_pixels": { + "name": "extra_pixels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "payout": { + "name": "payout", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_operation_id": { + "name": "grant_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "served_at": { + "name": "served_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "impression_fired_at": { + "name": "impression_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clicked_at": { + "name": "clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_ad_impression_user": { + "name": "idx_ad_impression_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ad_impression_imp_url": { + "name": "idx_ad_impression_imp_url", + "columns": [ + { + "expression": "imp_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ad_impression_user_id_user_id_fk": { + "name": "ad_impression_user_id_user_id_fk", + "tableFrom": "ad_impression", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ad_impression_imp_url_unique": { + "name": "ad_impression_imp_url_unique", + "nullsNotDistinct": false, + "columns": [ + "imp_url" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config": { + "name": "agent_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 1) AS INTEGER)", + "type": "stored" + } + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 2) AS INTEGER)", + "type": "stored" + } + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CAST(SPLIT_PART(\"agent_config\".\"version\", '.', 3) AS INTEGER)", + "type": "stored" + } + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": { + "idx_agent_config_publisher": { + "name": "idx_agent_config_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_publisher_id_publisher_id_fk": { + "name": "agent_config_publisher_id_publisher_id_fk", + "tableFrom": "agent_config", + "tableTo": "publisher", + "columnsFrom": [ + "publisher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_config_publisher_id_id_version_pk": { + "name": "agent_config_publisher_id_id_version_pk", + "columns": [ + "publisher_id", + "id", + "version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run": { + "name": "agent_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publisher_id": { + "name": "publisher_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '/', 1)\n ELSE NULL\n END", + "type": "stored" + } + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(split_part(agent_id, '/', 2), '@', 1)\n ELSE agent_id\n END", + "type": "stored" + } + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n WHEN agent_id ~ '^[^/@]+/[^/@]+@[^/@]+$'\n THEN split_part(agent_id, '@', 2)\n ELSE NULL\n END", + "type": "stored" + } + }, + "ancestor_run_ids": { + "name": "ancestor_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "root_run_id": { + "name": "root_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[1] ELSE id END", + "type": "stored" + } + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN array_length(ancestor_run_ids, 1) >= 1 THEN ancestor_run_ids[array_length(ancestor_run_ids, 1)] ELSE NULL END", + "type": "stored" + } + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "COALESCE(array_length(ancestor_run_ids, 1), 1)", + "type": "stored" + } + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "total_steps": { + "name": "total_steps", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "direct_credits": { + "name": "direct_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_credits": { + "name": "total_credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "status": { + "name": "status", + "type": "agent_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_agent_run_user_id": { + "name": "idx_agent_run_user_id", + "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": {} + }, + "idx_agent_run_parent": { + "name": "idx_agent_run_parent", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_root": { + "name": "idx_agent_run_root", + "columns": [ + { + "expression": "root_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_agent_id": { + "name": "idx_agent_run_agent_id", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_publisher": { + "name": "idx_agent_run_publisher", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_status": { + "name": "idx_agent_run_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_ancestors_gin": { + "name": "idx_agent_run_ancestors_gin", + "columns": [ + { + "expression": "ancestor_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_agent_run_completed_publisher_agent": { + "name": "idx_agent_run_completed_publisher_agent", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_recent": { + "name": "idx_agent_run_completed_recent", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_version": { + "name": "idx_agent_run_completed_version", + "columns": [ + { + "expression": "publisher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_run_completed_user": { + "name": "idx_agent_run_completed_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agent_run\".\"status\" = 'completed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_user_id_user_id_fk": { + "name": "agent_run_user_id_user_id_fk", + "tableFrom": "agent_run", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_step": { + "name": "agent_step", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "step_number": { + "name": "step_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN completed_at IS NOT NULL THEN EXTRACT(EPOCH FROM (completed_at - created_at)) * 1000 ELSE NULL END::integer", + "type": "stored" + } + }, + "credits": { + "name": "credits", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "child_run_ids": { + "name": "child_run_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spawned_count": { + "name": "spawned_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "array_length(child_run_ids, 1)", + "type": "stored" + } + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "agent_step_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "unique_step_number_per_run": { + "name": "unique_step_number_per_run", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "step_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_run_id": { + "name": "idx_agent_step_run_id", + "columns": [ + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_agent_step_children_gin": { + "name": "idx_agent_step_children_gin", + "columns": [ + { + "expression": "child_run_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "agent_step_agent_run_id_agent_run_id_fk": { + "name": "agent_step_agent_run_id_agent_run_id_fk", + "tableFrom": "agent_step", + "tableTo": "agent_run", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.composio_session": { + "name": "composio_session", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "composio_session_user_id_user_id_fk": { + "name": "composio_session_user_id_user_id_fk", + "tableFrom": "composio_session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "composio_session_session_id_unique": { + "name": "composio_session_session_id_unique", + "nullsNotDistinct": false, + "columns": [ + "session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_ledger": { + "name": "credit_ledger", + "schema": "", + "columns": { + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal": { + "name": "principal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "grant_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_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()" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_credit_ledger_active_balance": { + "name": "idx_credit_ledger_active_balance", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "balance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"credit_ledger\".\"balance\" != 0 AND \"credit_ledger\".\"expires_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_org": { + "name": "idx_credit_ledger_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_credit_ledger_subscription": { + "name": "idx_credit_ledger_subscription", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_ledger_user_id_user_id_fk": { + "name": "credit_ledger_user_id_user_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credit_ledger_org_id_org_id_fk": { + "name": "credit_ledger_org_id_org_id_fk", + "tableFrom": "credit_ledger", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.encrypted_api_keys": { + "name": "encrypted_api_keys", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "api_key_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "encrypted_api_keys_user_id_user_id_fk": { + "name": "encrypted_api_keys_user_id_user_id_fk", + "tableFrom": "encrypted_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "encrypted_api_keys_user_id_type_pk": { + "name": "encrypted_api_keys_user_id_type_pk", + "columns": [ + "user_id", + "type" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_mode_country_access_cache": { + "name": "free_mode_country_access_cache", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_ip_hash": { + "name": "client_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cf_country": { + "name": "cf_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geoip_country": { + "name": "geoip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_block_reason": { + "name": "country_block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_privacy_signals": { + "name": "ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spur_ip_privacy_signals": { + "name": "spur_ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "spur_status": { + "name": "spur_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scamalytics_ip_privacy_signals": { + "name": "scamalytics_ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "scamalytics_status": { + "name": "scamalytics_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scamalytics_score": { + "name": "scamalytics_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scamalytics_risk": { + "name": "scamalytics_risk", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_score": { + "name": "risk_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "privacy_decision": { + "name": "privacy_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "privacy_provider_decision": { + "name": "privacy_provider_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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": { + "idx_free_mode_country_cache_expires_at": { + "name": "idx_free_mode_country_cache_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_mode_country_access_cache_user_id_user_id_fk": { + "name": "free_mode_country_access_cache_user_id_user_id_fk", + "tableFrom": "free_mode_country_access_cache", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "free_mode_country_access_cache_user_id_client_ip_hash_pk": { + "name": "free_mode_country_access_cache_user_id_client_ip_hash_pk", + "columns": [ + "user_id", + "client_ip_hash" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session": { + "name": "free_session", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "free_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "active_instance_id": { + "name": "active_instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_tier": { + "name": "access_tier", + "type": "freebuff_access_tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cf_country": { + "name": "cf_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geoip_country": { + "name": "geoip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_block_reason": { + "name": "country_block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_privacy_signals": { + "name": "ip_privacy_signals", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_ip_hash": { + "name": "client_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_checked_at": { + "name": "country_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_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": { + "idx_free_session_queue": { + "name": "idx_free_session_queue", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_free_session_expiry": { + "name": "idx_free_session_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_user_id_user_id_fk": { + "name": "free_session_user_id_user_id_fk", + "tableFrom": "free_session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_session_admit": { + "name": "free_session_admit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_tier": { + "name": "access_tier", + "type": "freebuff_access_tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "session_units": { + "name": "session_units", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": true, + "default": "'1.0'" + } + }, + "indexes": { + "idx_free_session_admit_user_model_time": { + "name": "idx_free_session_admit_user_model_time", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "admitted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_session_admit_user_id_user_id_fk": { + "name": "free_session_admit_user_id_user_id_fk", + "tableFrom": "free_session_admit", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_eval_results": { + "name": "git_eval_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cost_mode": { + "name": "cost_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoner_model": { + "name": "reasoner_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_model": { + "name": "agent_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.limit_override": { + "name": "limit_override", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credits_per_block": { + "name": "credits_per_block", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "block_duration_hours": { + "name": "block_duration_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "weekly_credit_limit": { + "name": "weekly_credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "limit_override_user_id_user_id_fk": { + "name": "limit_override_user_id_user_id_fk", + "tableFrom": "limit_override", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "reasoning_text": { + "name": "reasoning_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byok": { + "name": "byok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ttft_ms": { + "name": "ttft_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "message_user_id_idx": { + "name": "message_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_finished_at_user_id_idx": { + "name": "message_finished_at_user_id_idx", + "columns": [ + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_idx": { + "name": "message_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_org_id_finished_at_idx": { + "name": "message_org_id_finished_at_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finished_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "message_org_id_org_id_fk": { + "name": "message_org_id_org_id_fk", + "tableFrom": "message", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org": { + "name": "org", + "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 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_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 + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_limit": { + "name": "credit_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_alerts": { + "name": "billing_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "weekly_reports": { + "name": "weekly_reports", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "org_owner_id_user_id_fk": { + "name": "org_owner_id_user_id_fk", + "tableFrom": "org", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_slug_unique": { + "name": "org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + }, + "org_stripe_customer_id_unique": { + "name": "org_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_feature": { + "name": "org_feature", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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": { + "idx_org_feature_active": { + "name": "idx_org_feature_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_feature_org_id_org_id_fk": { + "name": "org_feature_org_id_org_id_fk", + "tableFrom": "org_feature", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_feature_org_id_feature_pk": { + "name": "org_feature_org_id_feature_pk", + "columns": [ + "org_id", + "feature" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invite": { + "name": "org_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_org_invite_token": { + "name": "idx_org_invite_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_email": { + "name": "idx_org_invite_email", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_invite_expires": { + "name": "idx_org_invite_expires", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invite_org_id_org_id_fk": { + "name": "org_invite_org_id_org_id_fk", + "tableFrom": "org_invite", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invite_invited_by_user_id_fk": { + "name": "org_invite_invited_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "org_invite_accepted_by_user_id_fk": { + "name": "org_invite_accepted_by_user_id_fk", + "tableFrom": "org_invite", + "tableTo": "user", + "columnsFrom": [ + "accepted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invite_token_unique": { + "name": "org_invite_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_member": { + "name": "org_member", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "org_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_member_org_id_org_id_fk": { + "name": "org_member_org_id_org_id_fk", + "tableFrom": "org_member", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_member_user_id_user_id_fk": { + "name": "org_member_user_id_user_id_fk", + "tableFrom": "org_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_member_org_id_user_id_pk": { + "name": "org_member_org_id_user_id_pk", + "columns": [ + "org_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_repo": { + "name": "org_repo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_org_repo_active": { + "name": "idx_org_repo_active", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_org_repo_unique": { + "name": "idx_org_repo_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_repo_org_id_org_id_fk": { + "name": "org_repo_org_id_org_id_fk", + "tableFrom": "org_repo", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_repo_approved_by_user_id_fk": { + "name": "org_repo_approved_by_user_id_fk", + "tableFrom": "org_repo", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publisher": { + "name": "publisher", + "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": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": {}, + "foreignKeys": { + "publisher_user_id_user_id_fk": { + "name": "publisher_user_id_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_org_id_org_id_fk": { + "name": "publisher_org_id_org_id_fk", + "tableFrom": "publisher", + "tableTo": "org", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "publisher_created_by_user_id_fk": { + "name": "publisher_created_by_user_id_fk", + "tableFrom": "publisher", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "publisher_single_owner": { + "name": "publisher_single_owner", + "value": "(\"publisher\".\"user_id\" IS NOT NULL AND \"publisher\".\"org_id\" IS NULL) OR\n (\"publisher\".\"user_id\" IS NULL AND \"publisher\".\"org_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_legacy": { + "name": "is_legacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referrer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referred_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": [ + "referrer_id", + "referred_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_auth_hash": { + "name": "cli_auth_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "session_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'web'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_cli_auth_code_idx": { + "name": "session_cli_auth_code_idx", + "columns": [ + { + "expression": "fingerprint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cli_auth_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"session\".\"cli_auth_hash\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": [ + "fingerprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_tier": { + "name": "scheduled_tier", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canceled_at": { + "name": "canceled_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": { + "idx_subscription_customer": { + "name": "idx_subscription_customer", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_user": { + "name": "idx_subscription_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_subscription_status": { + "name": "idx_subscription_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_user_id_user_id_fk": { + "name": "subscription_user_id_user_id_fk", + "tableFrom": "subscription", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_failure": { + "name": "sync_failure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_sync_failure_retry": { + "name": "idx_sync_failure_retry", + "columns": [ + { + "expression": "retry_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sync_failure\".\"retry_count\" < 5", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + }, + "referral_limit": { + "name": "referral_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "discord_id": { + "name": "discord_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_topup_enabled": { + "name": "auto_topup_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auto_topup_threshold": { + "name": "auto_topup_threshold", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_topup_amount": { + "name": "auto_topup_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fallback_to_a_la_carte": { + "name": "fallback_to_a_la_carte", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": [ + "referral_code" + ] + }, + "user_discord_id_unique": { + "name": "user_discord_id_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_id" + ] + }, + "user_handle_unique": { + "name": "user_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.agent_run_status": { + "name": "agent_run_status", + "schema": "public", + "values": [ + "running", + "completed", + "failed", + "cancelled" + ] + }, + "public.agent_step_status": { + "name": "agent_step_status", + "schema": "public", + "values": [ + "running", + "completed", + "skipped" + ] + }, + "public.api_key_type": { + "name": "api_key_type", + "schema": "public", + "values": [ + "anthropic", + "gemini", + "openai" + ] + }, + "public.free_session_status": { + "name": "free_session_status", + "schema": "public", + "values": [ + "queued", + "active" + ] + }, + "public.freebuff_access_tier": { + "name": "freebuff_access_tier", + "schema": "public", + "values": [ + "full", + "limited" + ] + }, + "public.grant_type": { + "name": "grant_type", + "schema": "public", + "values": [ + "free", + "referral", + "referral_legacy", + "subscription", + "purchase", + "admin", + "organization", + "ad" + ] + }, + "public.org_role": { + "name": "org_role", + "schema": "public", + "values": [ + "owner", + "admin", + "member" + ] + }, + "public.session_type": { + "name": "session_type", + "schema": "public", + "values": [ + "web", + "pat", + "cli" + ] + }, + "public.subscription_status": { + "name": "subscription_status", + "schema": "public", + "values": [ + "incomplete", + "incomplete_expired", + "trialing", + "active", + "past_due", + "canceled", + "unpaid", + "paused" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/internal/src/db/migrations/meta/_journal.json b/packages/internal/src/db/migrations/meta/_journal.json index 91e6898f02..72312bacf8 100644 --- a/packages/internal/src/db/migrations/meta/_journal.json +++ b/packages/internal/src/db/migrations/meta/_journal.json @@ -393,6 +393,13 @@ "when": 1779661395724, "tag": "0055_glossy_gertrude_yorkes", "breakpoints": true + }, + { + "idx": 56, + "version": "7", + "when": 1779667810810, + "tag": "0056_simple_tyrannus", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/internal/src/db/schema.ts b/packages/internal/src/db/schema.ts index 9fd30f3139..ef93b70f57 100644 --- a/packages/internal/src/db/schema.ts +++ b/packages/internal/src/db/schema.ts @@ -305,6 +305,19 @@ export const encryptedApiKeys = pgTable( }), ) +export const composioSession = pgTable('composio_session', { + user_id: text('user_id') + .primaryKey() + .references(() => user.id, { onDelete: 'cascade' }), + session_id: text('session_id').notNull().unique(), + created_at: timestamp('created_at', { mode: 'date', withTimezone: true }) + .notNull() + .defaultNow(), + updated_at: timestamp('updated_at', { mode: 'date', withTimezone: true }) + .notNull() + .defaultNow(), +}) + // Organization tables export const orgRoleEnum = pgEnum('org_role', ['owner', 'admin', 'member']) diff --git a/packages/internal/src/env-schema.ts b/packages/internal/src/env-schema.ts index 644426b879..d42cb445bd 100644 --- a/packages/internal/src/env-schema.ts +++ b/packages/internal/src/env-schema.ts @@ -18,6 +18,7 @@ export const serverEnvSchema = clientEnvSchema.extend({ IPINFO_TOKEN: z.string().min(1), SPUR_TOKEN: z.string().min(1), SCAMALYTICS_API_KEY: z.string().min(1), + COMPOSIO_API_KEY: z.string().min(1).optional(), // ZeroClick tenant API key used for server-side offer fallback requests. ZEROCLICK_API_KEY: z.string().min(1).optional(), // BuySellAds (Carbon) zone key used for the Freebuff waiting-room ad. @@ -112,6 +113,7 @@ export const serverProcessEnv: ServerInput = { IPINFO_TOKEN: process.env.IPINFO_TOKEN, SPUR_TOKEN: process.env.SPUR_TOKEN, SCAMALYTICS_API_KEY: process.env.SCAMALYTICS_API_KEY, + COMPOSIO_API_KEY: process.env.COMPOSIO_API_KEY, ZEROCLICK_API_KEY: process.env.ZEROCLICK_API_KEY, CARBON_ZONE_KEY: process.env.CARBON_ZONE_KEY, PORT: process.env.PORT, diff --git a/sdk/src/__tests__/composio.test.ts b/sdk/src/__tests__/composio.test.ts new file mode 100644 index 0000000000..b9447d479c --- /dev/null +++ b/sdk/src/__tests__/composio.test.ts @@ -0,0 +1,87 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test' + +import { COMPOSIO_META_TOOL_NAMES } from '@codebuff/common/constants/composio' +import { clientToolNames, toolParams } from '@codebuff/common/tools/list' + +import { executeComposioToolViaServer } from '../composio' + +describe('Composio SDK tools', () => { + const originalFetch = globalThis.fetch + + afterEach(() => { + globalThis.fetch = originalFetch + }) + + test('registers Composio meta tools as static client tools without discovery fetch', () => { + const fetchMock = mock(async () => new Response('{}')) + globalThis.fetch = fetchMock as unknown as typeof fetch + + for (const toolName of COMPOSIO_META_TOOL_NAMES) { + expect(clientToolNames).toContain(toolName) + expect(toolParams[toolName].inputSchema).toBeDefined() + } + expect(fetchMock).not.toHaveBeenCalled() + }) + + test('executes a meta tool through the server execute endpoint', async () => { + const fetchMock = mock( + async (_url: string | URL | Request, init?: RequestInit) => { + expect(init?.method).toBe('POST') + expect(init?.headers).toEqual({ + Authorization: 'Bearer codebuff-api-key', + 'Content-Type': 'application/json', + }) + expect(JSON.parse(String(init?.body))).toEqual({ + toolName: 'composio_search_tools', + input: { + queries: ['find gmail tools'], + session: { generate_id: true }, + }, + }) + return new Response( + JSON.stringify({ + output: [{ type: 'json', value: { ok: true } }], + }), + { status: 200 }, + ) + }, + ) + globalThis.fetch = fetchMock as unknown as typeof fetch + + const output = await executeComposioToolViaServer({ + apiKey: 'codebuff-api-key', + toolName: 'composio_search_tools', + input: { + queries: ['find gmail tools'], + session: { generate_id: true }, + }, + }) + + expect(output).toEqual([{ type: 'json', value: { ok: true } }]) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + test('returns a tool error when the server response is malformed', async () => { + globalThis.fetch = mock( + async () => new Response(JSON.stringify({ ok: true }), { status: 200 }), + ) as unknown as typeof fetch + + const output = await executeComposioToolViaServer({ + apiKey: 'codebuff-api-key', + toolName: 'composio_search_tools', + input: { + queries: ['find gmail tools'], + session: { generate_id: true }, + }, + }) + + expect(output).toEqual([ + { + type: 'json', + value: { + errorMessage: 'Invalid Composio execute response from server', + }, + }, + ]) + }) +}) diff --git a/sdk/src/composio.ts b/sdk/src/composio.ts new file mode 100644 index 0000000000..a19d9da23f --- /dev/null +++ b/sdk/src/composio.ts @@ -0,0 +1,77 @@ +import { WEBSITE_URL } from './constants' + +import type { ComposioMetaToolName } from '@codebuff/common/constants/composio' +import type { ToolResultOutput } from '@codebuff/common/types/messages/content-part' + +type ComposioExecuteResponse = { + output: ToolResultOutput[] +} + +async function readErrorMessage(response: Response): Promise { + try { + const body = (await response.json()) as { + error?: unknown + message?: unknown + } + return String(body.error ?? body.message ?? response.statusText) + } catch { + return response.statusText + } +} + +export async function executeComposioToolViaServer(params: { + apiKey: string + toolName: ComposioMetaToolName + input: Record +}): Promise { + try { + const response = await fetch( + new URL('/api/v1/composio/execute', WEBSITE_URL), + { + method: 'POST', + headers: { + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + toolName: params.toolName, + input: params.input, + }), + }, + ) + + if (!response.ok) { + return [ + { + type: 'json', + value: { + errorMessage: await readErrorMessage(response), + status: response.status, + }, + }, + ] + } + + const body = (await response.json()) as ComposioExecuteResponse + if (!Array.isArray(body.output)) { + return [ + { + type: 'json', + value: { + errorMessage: 'Invalid Composio execute response from server', + }, + }, + ] + } + return body.output + } catch (error) { + return [ + { + type: 'json', + value: { + errorMessage: error instanceof Error ? error.message : String(error), + }, + }, + ] + } +} diff --git a/sdk/src/run.ts b/sdk/src/run.ts index 4014e85449..3b6d221ff3 100644 --- a/sdk/src/run.ts +++ b/sdk/src/run.ts @@ -12,12 +12,17 @@ import { listMCPTools, callMCPTool, } from '@codebuff/common/mcp/client' +import { + COMPOSIO_META_TOOL_NAMES, + isComposioMetaToolName, +} from '@codebuff/common/constants/composio' import { toolNames } from '@codebuff/common/tools/constants' import { clientToolCallSchema } from '@codebuff/common/tools/list' import { AgentOutputSchema } from '@codebuff/common/types/session-state' import { extractApiErrorDetails } from '@codebuff/common/util/error' import { cloneDeep } from 'lodash' +import { executeComposioToolViaServer } from './composio' import { getErrorStatusCode } from './error-utils' import { getAgentRuntimeImpl } from './impl/agent-runtime' import { getUserInfoFromApiKey } from './impl/database' @@ -37,16 +42,8 @@ import type { RunState } from './run-state' import type { FileFilter } from './tools/read-files' import type { ServerAction } from '@codebuff/common/actions' import type { AgentDefinition } from '@codebuff/common/templates/initial-agents-dir/types/agent-definition' -import type { - PublishedToolName, - ToolName, -} from '@codebuff/common/tools/constants' -import type { - ClientToolCall, - ClientToolName, - CodebuffToolOutput, - PublishedClientToolName, -} from '@codebuff/common/tools/list' +import type { ToolName } from '@codebuff/common/tools/constants' +import type { PublishedClientToolName } from '@codebuff/common/tools/list' import type { Logger } from '@codebuff/common/types/contracts/logger' import type { CodebuffFileSystem } from '@codebuff/common/types/filesystem' import type { ToolMessage } from '@codebuff/common/types/messages/codebuff-message' @@ -74,6 +71,15 @@ const wrapContentForUserMessage = ( return buildUserMessageContent(undefined, undefined, content) } +type OverrideToolHandlers = { + [K in PublishedClientToolName]?: (input: any) => Promise +} & { + // Include read_files separately, since it has a different signature. + read_files?: (input: { + filePaths: string[] + }) => Promise> +} + export type CodebuffClientOptions = { apiKey?: string @@ -107,18 +113,7 @@ export type CodebuffClientOptions = { /** Optional filter to classify files before reading (runs before gitignore check) */ fileFilter?: FileFilter - overrideTools?: Partial< - { - [K in ClientToolName & PublishedToolName]: ( - input: ClientToolCall['input'], - ) => Promise> - } & { - // Include read_files separately, since it has a different signature. - read_files: (input: { - filePaths: string[] - }) => Promise> - } - > + overrideTools?: OverrideToolHandlers customToolDefinitions?: CustomToolDefinition[] fsSource?: Source @@ -233,6 +228,7 @@ async function runOnce({ spawn = require('child_process').spawn as CodebuffSpawn } const preparedContent = wrapContentForUserMessage(content) + let activeCustomToolDefinitions = customToolDefinitions ?? [] // Init session state let agentId @@ -274,6 +270,10 @@ async function runOnce({ } const traceSessionId = previousRun?.traceSessionId ?? crypto.randomUUID() + for (const toolName of COMPOSIO_META_TOOL_NAMES) { + delete sessionState.fileContext.customToolDefinitions[toolName] + } + let resolve: (value: RunReturnType) => any = () => {} let _reject: (error: any) => any = () => {} const promise = new Promise((res, rej) => { @@ -292,7 +292,7 @@ async function runOnce({ // Comparing array identity detects progress more robustly than length: // context pruning could shrink history below its starting length without // meaning the runtime never ran. - const initialMessageHistory = sessionState.mainAgentState.messageHistory + let initialMessageHistory = sessionState.mainAgentState.messageHistory /** Calculates the current session state if cancelled. * @@ -396,14 +396,15 @@ async function runOnce({ mcpConfig, }, overrides: overrideTools ?? {}, - customToolDefinitions: customToolDefinitions + customToolDefinitions: activeCustomToolDefinitions ? Object.fromEntries( - customToolDefinitions.map((def) => [def.toolName, def]), + activeCustomToolDefinitions.map((def) => [def.toolName, def]), ) : {}, cwd, fs, env, + apiKey, }) }, requestMcpToolData: async ({ mcpConfig, toolNames }) => { @@ -511,7 +512,6 @@ async function runOnce({ if (!userInfo) { return getCancelledRunState('Invalid API key or user not found') } - const userId = userInfo.id if (signal?.aborted) { @@ -618,6 +618,7 @@ async function handleToolCall({ cwd, fs, env, + apiKey, }: { action: ServerAction<'tool-call-request'> overrides: NonNullable @@ -625,6 +626,7 @@ async function handleToolCall({ cwd?: string fs: CodebuffFileSystem env?: Record + apiKey: string }): Promise<{ output: ToolResultOutput[] }> { const toolName = action.toolName const input = action.input @@ -735,6 +737,12 @@ async function handleToolCall({ }, }, ] + } else if (isComposioMetaToolName(toolName)) { + result = await executeComposioToolViaServer({ + apiKey, + toolName, + input, + }) } else { throw new Error( `Tool not implemented in SDK. Please provide an override or modify your agent to not use this tool: ${toolName}`, diff --git a/web/package.json b/web/package.json index 830cbbdc36..e1b947073e 100644 --- a/web/package.json +++ b/web/package.json @@ -42,6 +42,7 @@ "@codebuff/common": "workspace:*", "@codebuff/internal": "workspace:*", "@codebuff/sdk": "workspace:*", + "@composio/core": "^0.10.0", "@hookform/resolvers": "^3.9.0", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", diff --git a/web/src/app/api/v1/composio/__tests__/composio.test.ts b/web/src/app/api/v1/composio/__tests__/composio.test.ts new file mode 100644 index 0000000000..b29a4cb021 --- /dev/null +++ b/web/src/app/api/v1/composio/__tests__/composio.test.ts @@ -0,0 +1,269 @@ +import { + describe, + expect, + mock, + test, + beforeAll, + beforeEach, + afterEach, +} from 'bun:test' +import { NextRequest } from 'next/server' + +import type { GetUserInfoFromApiKeyFn } from '@codebuff/common/types/contracts/database' +import type { + Logger, + LoggerWithContextFn, +} from '@codebuff/common/types/contracts/logger' +import type { postComposioExecute as PostComposioExecute } from '../execute/_post' + +let postComposioExecute: typeof PostComposioExecute + +function setEnvDefault(key: string, value: string) { + process.env[key] ??= value +} + +beforeAll(async () => { + setEnvDefault('CI', 'true') + setEnvDefault('NEXT_PUBLIC_CB_ENVIRONMENT', 'test') + setEnvDefault('NEXT_PUBLIC_CODEBUFF_APP_URL', 'https://codebuff.test') + setEnvDefault('NEXT_PUBLIC_SUPPORT_EMAIL', 'support@codebuff.test') + setEnvDefault('NEXT_PUBLIC_POSTHOG_API_KEY', 'test-posthog-key') + setEnvDefault('NEXT_PUBLIC_POSTHOG_HOST_URL', 'https://posthog.test') + setEnvDefault('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', 'pk_test') + setEnvDefault( + 'NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL', + 'https://stripe.test/portal', + ) + setEnvDefault('NEXT_PUBLIC_WEB_PORT', '3000') + + mock.module('server-only', () => ({})) + ;({ postComposioExecute } = await import('../execute/_post')) +}) + +describe('/api/v1/composio', () => { + const mockDb = {} as any + let logger: Logger + let loggerWithContext: LoggerWithContextFn + let getUserInfoFromApiKey: GetUserInfoFromApiKeyFn + + beforeEach(() => { + logger = { + error: mock(() => {}), + warn: mock(() => {}), + info: mock(() => {}), + debug: mock(() => {}), + } + loggerWithContext = mock(() => logger) + getUserInfoFromApiKey = mock(async ({ apiKey }) => { + if (apiKey === 'banned-key') { + return { + id: 'banned-user', + email: 'banned@example.com', + discord_id: null, + banned: true, + } as Awaited> + } + if (apiKey !== 'valid-key') return null + return { + id: 'user-123', + email: 'user@example.com', + discord_id: null, + banned: false, + } as Awaited> + }) + }) + + afterEach(() => { + mock.restore() + }) + + test('executes a Composio tool for an authenticated user', async () => { + const executeTool = mock(async () => [ + { type: 'json' as const, value: { ok: true } }, + ]) + const checkRateLimit = mock(() => ({ limited: false as const })) + const req = new NextRequest('http://localhost/api/v1/composio/execute', { + method: 'POST', + headers: { Authorization: 'Bearer valid-key' }, + body: JSON.stringify({ + toolName: 'composio_search_tools', + input: { + queries: ['find gmail tools'], + session: { generate_id: true }, + }, + }), + }) + + const response = await postComposioExecute({ + req, + getUserInfoFromApiKey, + db: mockDb, + logger, + loggerWithContext, + executeTool, + checkRateLimit, + isConfigured: () => true, + }) + + expect(response.status).toBe(200) + expect(executeTool).toHaveBeenCalledWith({ + db: mockDb, + userId: 'user-123', + logger, + toolName: 'composio_search_tools', + input: { + queries: ['find gmail tools'], + session: { generate_id: true }, + }, + }) + expect(checkRateLimit).toHaveBeenCalledWith('user-123') + }) + + test('returns 503 when Composio execute is not configured', async () => { + const executeTool = mock(async () => [ + { type: 'json' as const, value: { ok: true } }, + ]) + const req = new NextRequest('http://localhost/api/v1/composio/execute', { + method: 'POST', + headers: { Authorization: 'Bearer valid-key' }, + body: JSON.stringify({ + toolName: 'composio_search_tools', + input: {}, + }), + }) + + const response = await postComposioExecute({ + req, + getUserInfoFromApiKey, + db: mockDb, + logger, + loggerWithContext, + executeTool, + checkRateLimit: mock(() => ({ limited: false as const })), + isConfigured: () => false, + }) + + expect(response.status).toBe(503) + expect(await response.json()).toEqual({ + error: 'Composio is not configured', + }) + expect(executeTool).not.toHaveBeenCalled() + }) + + test('rate limits Composio execute requests', async () => { + const executeTool = mock(async () => [ + { type: 'json' as const, value: { ok: true } }, + ]) + const req = new NextRequest('http://localhost/api/v1/composio/execute', { + method: 'POST', + headers: { Authorization: 'Bearer valid-key' }, + body: JSON.stringify({ + toolName: 'composio_search_tools', + input: {}, + }), + }) + + const response = await postComposioExecute({ + req, + getUserInfoFromApiKey, + db: mockDb, + logger, + loggerWithContext, + executeTool, + checkRateLimit: mock(() => ({ + limited: true as const, + retryAfterMs: 1_000, + windowName: '1 minute', + })), + isConfigured: () => true, + }) + + expect(response.status).toBe(429) + expect(await response.json()).toEqual({ + error: 'Rate limited', + retryAfterSeconds: 1, + }) + expect(executeTool).not.toHaveBeenCalled() + }) + + test('rejects unsupported Composio tool names before execution', async () => { + const executeTool = mock(async () => [ + { type: 'json' as const, value: { ok: true } }, + ]) + const req = new NextRequest('http://localhost/api/v1/composio/execute', { + method: 'POST', + headers: { Authorization: 'Bearer valid-key' }, + body: JSON.stringify({ + toolName: 'COMPOSIO_REMOTE_WORKBENCH', + input: {}, + }), + }) + + const response = await postComposioExecute({ + req, + getUserInfoFromApiKey, + db: mockDb, + logger, + loggerWithContext, + executeTool, + checkRateLimit: mock(() => ({ limited: false as const })), + isConfigured: () => true, + }) + + expect(response.status).toBe(400) + expect(executeTool).not.toHaveBeenCalled() + }) + + test('rejects unauthenticated Composio requests', async () => { + const req = new NextRequest('http://localhost/api/v1/composio/execute', { + method: 'POST', + }) + + const response = await postComposioExecute({ + req, + getUserInfoFromApiKey, + db: mockDb, + logger, + loggerWithContext, + executeTool: mock(async () => [{ type: 'json' as const, value: {} }]), + checkRateLimit: mock(() => ({ limited: false as const })), + isConfigured: () => true, + }) + + expect(response.status).toBe(401) + expect(await response.json()).toEqual({ + error: 'Missing or invalid Authorization header', + }) + }) + + test('rejects suspended users before rate limiting or tool execution', async () => { + const executeTool = mock(async () => [{ type: 'json' as const, value: {} }]) + const checkRateLimit = mock(() => ({ limited: false as const })) + const req = new NextRequest('http://localhost/api/v1/composio/execute', { + method: 'POST', + headers: { Authorization: 'Bearer banned-key' }, + body: JSON.stringify({ + toolName: 'composio_search_tools', + input: {}, + }), + }) + + const response = await postComposioExecute({ + req, + getUserInfoFromApiKey, + db: mockDb, + logger, + loggerWithContext, + executeTool, + checkRateLimit, + isConfigured: () => true, + }) + + expect(response.status).toBe(403) + const body = await response.json() + expect(body.error).toBe('account_suspended') + expect(body.message).toContain('Your account has been suspended') + expect(executeTool).not.toHaveBeenCalled() + expect(checkRateLimit).not.toHaveBeenCalled() + }) +}) diff --git a/web/src/app/api/v1/composio/_auth.ts b/web/src/app/api/v1/composio/_auth.ts new file mode 100644 index 0000000000..9150cab7a9 --- /dev/null +++ b/web/src/app/api/v1/composio/_auth.ts @@ -0,0 +1,75 @@ +import { NextResponse } from 'next/server' +import { env } from '@codebuff/internal/env' + +import type { GetUserInfoFromApiKeyFn } from '@codebuff/common/types/contracts/database' +import type { + Logger, + LoggerWithContextFn, +} from '@codebuff/common/types/contracts/logger' +import type { NextRequest } from 'next/server' + +import { extractApiKeyFromHeader } from '@/util/auth' + +type ComposioUser = { + id: string + email: string + discord_id: string | null + banned: boolean +} + +export async function requireComposioUser(params: { + req: NextRequest + getUserInfoFromApiKey: GetUserInfoFromApiKeyFn + logger: Logger + loggerWithContext: LoggerWithContextFn +}): Promise< + | { ok: true; userInfo: ComposioUser; logger: Logger } + | { ok: false; response: NextResponse } +> { + const { req, getUserInfoFromApiKey, logger, loggerWithContext } = params + + const apiKey = extractApiKeyFromHeader(req) + if (!apiKey) { + return { + ok: false, + response: NextResponse.json( + { error: 'Missing or invalid Authorization header' }, + { status: 401 }, + ), + } + } + + const userInfo = await getUserInfoFromApiKey({ + apiKey, + fields: ['id', 'email', 'discord_id', 'banned'], + logger, + }) + if (!userInfo) { + return { + ok: false, + response: NextResponse.json( + { error: 'Invalid API key or user not found' }, + { status: 401 }, + ), + } + } + + if (userInfo.banned) { + return { + ok: false, + response: NextResponse.json( + { + error: 'account_suspended', + message: `Your account has been suspended. Please contact ${env.NEXT_PUBLIC_SUPPORT_EMAIL} if you did not expect this.`, + }, + { status: 403 }, + ), + } + } + + return { + ok: true, + userInfo, + logger: loggerWithContext({ userInfo }), + } +} diff --git a/web/src/app/api/v1/composio/execute/_post.ts b/web/src/app/api/v1/composio/execute/_post.ts new file mode 100644 index 0000000000..0d85407dd6 --- /dev/null +++ b/web/src/app/api/v1/composio/execute/_post.ts @@ -0,0 +1,131 @@ +import { getErrorObject } from '@codebuff/common/util/error' +import { COMPOSIO_META_TOOL_NAMES } from '@codebuff/common/constants/composio' +import { NextResponse } from 'next/server' +import { z } from 'zod/v4' + +import type { GetUserInfoFromApiKeyFn } from '@codebuff/common/types/contracts/database' +import type { + Logger, + LoggerWithContextFn, +} from '@codebuff/common/types/contracts/logger' +import type { CodebuffPgDatabase } from '@codebuff/internal/db/types' +import type { NextRequest } from 'next/server' + +import { executeComposioTool, isComposioConfigured } from '@/server/composio' +import { checkComposioRateLimit } from '@/server/composio-rate-limiter' + +import { requireComposioUser } from '../_auth' + +type ExecuteComposioToolFn = typeof executeComposioTool +type CheckComposioRateLimitFn = typeof checkComposioRateLimit +type IsComposioConfiguredFn = typeof isComposioConfigured + +const composioExecuteBodySchema = z.object({ + toolName: z.enum(COMPOSIO_META_TOOL_NAMES), + input: z.record(z.string(), z.unknown()).default({}), +}) + +export async function postComposioExecute(params: { + req: NextRequest + getUserInfoFromApiKey: GetUserInfoFromApiKeyFn + db: CodebuffPgDatabase + logger: Logger + loggerWithContext: LoggerWithContextFn + executeTool?: ExecuteComposioToolFn + checkRateLimit?: CheckComposioRateLimitFn + isConfigured?: IsComposioConfiguredFn +}) { + const { + db, + executeTool = executeComposioTool, + checkRateLimit = checkComposioRateLimit, + isConfigured = isComposioConfigured, + } = params + const auth = await requireComposioUser(params) + if (!auth.ok) return auth.response + const { userInfo, logger } = auth + const { req } = params + + if (!isConfigured()) { + return NextResponse.json( + { error: 'Composio is not configured' }, + { status: 503 }, + ) + } + + const rateLimit = checkRateLimit(userInfo.id) + if (rateLimit.limited) { + const retryAfterSeconds = Math.ceil(rateLimit.retryAfterMs / 1000) + logger.warn( + { + userId: userInfo.id, + retryAfterSeconds, + windowName: rateLimit.windowName, + }, + 'Rate limited Composio execute request', + ) + return NextResponse.json( + { error: 'Rate limited', retryAfterSeconds }, + { + status: 429, + headers: { 'Retry-After': String(retryAfterSeconds) }, + }, + ) + } + + let json: unknown + try { + json = await req.json() + } catch { + return NextResponse.json( + { error: 'Invalid JSON in request body' }, + { status: 400 }, + ) + } + + const parsed = composioExecuteBodySchema.safeParse(json) + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid request body', details: parsed.error.format() }, + { status: 400 }, + ) + } + + try { + logger.info( + { userId: userInfo.id, toolName: parsed.data.toolName }, + 'Executing Composio tool', + ) + const output = await executeTool({ + db, + userId: userInfo.id, + logger, + ...parsed.data, + }) + if (!output) { + return NextResponse.json( + { error: 'Composio is not configured' }, + { status: 503 }, + ) + } + + logger.info( + { + userId: userInfo.id, + toolName: parsed.data.toolName, + outputCount: output.length, + }, + 'Executed Composio tool', + ) + return NextResponse.json({ output }) + } catch (error) { + logger.error( + { error: getErrorObject(error), userId: userInfo.id }, + 'Failed to execute Composio tool', + ) + return NextResponse.json( + { error: 'Failed to execute Composio tool' }, + { status: 502 }, + ) + } +} diff --git a/web/src/app/api/v1/composio/execute/route.ts b/web/src/app/api/v1/composio/execute/route.ts new file mode 100644 index 0000000000..ab64001d2b --- /dev/null +++ b/web/src/app/api/v1/composio/execute/route.ts @@ -0,0 +1,18 @@ +import db from '@codebuff/internal/db' + +import { getUserInfoFromApiKey } from '@/db/user' +import { logger, loggerWithContext } from '@/util/logger' + +import { postComposioExecute } from './_post' + +import type { NextRequest } from 'next/server' + +export async function POST(req: NextRequest) { + return postComposioExecute({ + req, + getUserInfoFromApiKey, + db, + logger, + loggerWithContext, + }) +} diff --git a/web/src/server/__tests__/composio-rate-limiter.test.ts b/web/src/server/__tests__/composio-rate-limiter.test.ts new file mode 100644 index 0000000000..dd36e26720 --- /dev/null +++ b/web/src/server/__tests__/composio-rate-limiter.test.ts @@ -0,0 +1,33 @@ +import { beforeEach, describe, expect, test } from 'bun:test' + +import { + checkComposioRateLimit, + resetComposioRateLimits, +} from '../composio-rate-limiter' + +describe('checkComposioRateLimit', () => { + beforeEach(() => { + resetComposioRateLimits() + }) + + test('allows requests below the per-minute limit', () => { + for (let i = 0; i < 120; i++) { + expect(checkComposioRateLimit('user-1')).toEqual({ + limited: false, + }) + } + }) + + test('limits execution after the per-minute limit', () => { + for (let i = 0; i < 120; i++) { + checkComposioRateLimit('user-1') + } + + const result = checkComposioRateLimit('user-1') + expect(result.limited).toBe(true) + if (result.limited) { + expect(result.windowName).toBe('1 minute') + expect(result.retryAfterMs).toBeGreaterThan(0) + } + }) +}) diff --git a/web/src/server/__tests__/composio.test.ts b/web/src/server/__tests__/composio.test.ts new file mode 100644 index 0000000000..356b678c79 --- /dev/null +++ b/web/src/server/__tests__/composio.test.ts @@ -0,0 +1,206 @@ +import { beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test' + +import type { Logger } from '@codebuff/common/types/contracts/logger' +import type { executeComposioTool as ExecuteComposioTool } from '../composio' + +let executeComposioTool: typeof ExecuteComposioTool + +let createSession: ReturnType +let useSession: ReturnType +let execute: ReturnType + +function setEnvDefault(key: string, value: string) { + process.env[key] ??= value +} + +beforeAll(async () => { + setEnvDefault('CI', 'true') + setEnvDefault('NEXT_PUBLIC_CB_ENVIRONMENT', 'test') + setEnvDefault('NEXT_PUBLIC_CODEBUFF_APP_URL', 'https://codebuff.test') + setEnvDefault('NEXT_PUBLIC_SUPPORT_EMAIL', 'support@codebuff.test') + setEnvDefault('NEXT_PUBLIC_POSTHOG_API_KEY', 'test-posthog-key') + setEnvDefault('NEXT_PUBLIC_POSTHOG_HOST_URL', 'https://posthog.test') + setEnvDefault('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', 'pk_test') + setEnvDefault( + 'NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL', + 'https://stripe.test/portal', + ) + setEnvDefault('NEXT_PUBLIC_WEB_PORT', '3000') + + mock.module('server-only', () => ({})) + mock.module('@composio/core', () => ({ + Composio: class { + create = createSession + use = useSession + }, + })) + ;({ executeComposioTool } = await import('../composio')) +}) + +describe('executeComposioTool', () => { + let logger: Logger + + beforeEach(() => { + logger = { + error: mock(() => {}), + warn: mock(() => {}), + info: mock(() => {}), + debug: mock(() => {}), + } + execute = mock(async () => ({ ok: true })) + createSession = mock(async () => ({ sessionId: 'fresh-session', execute })) + useSession = mock(async () => ({ sessionId: 'stored-session', execute })) + }) + + function makeDb(storedSessionIds: string | null | Array) { + const storedSessionIdSequence = Array.isArray(storedSessionIds) + ? [...storedSessionIds] + : [storedSessionIds] + const findFirst = mock(async () => { + const storedSessionId = + storedSessionIdSequence.length > 1 + ? storedSessionIdSequence.shift() + : storedSessionIdSequence[0] + + return storedSessionId + ? { + user_id: 'user-123', + session_id: storedSessionId, + created_at: new Date(), + updated_at: new Date(), + } + : null + }) + const onConflictDoNothing = mock(async () => undefined) + const values = mock(() => ({ onConflictDoNothing })) + const whereDelete = mock(async () => undefined) + + return { + db: { + query: { + composioSession: { + findFirst, + }, + }, + insert: mock(() => ({ values })), + delete: mock(() => ({ where: whereDelete })), + } as any, + findFirst, + onConflictDoNothing, + values, + whereDelete, + } + } + + test('replaces a stored session when Composio can no longer rehydrate it', async () => { + const notFound = Object.assign(new Error('Composio session not found'), { + status: 404, + }) + useSession = mock(async () => { + throw notFound + }) + const { db, whereDelete, values } = makeDb([ + 'stored-session', + 'fresh-session', + ]) + + const result = await executeComposioTool({ + db, + userId: 'user-123', + logger, + apiKey: 'test-composio-api-key', + toolName: 'composio_search_tools', + input: { queries: ['gmail'], session: { generate_id: true } }, + }) + + expect(result).toEqual([{ type: 'json', value: { ok: true } }]) + expect(useSession).toHaveBeenCalledWith('stored-session') + expect(whereDelete).toHaveBeenCalledTimes(1) + expect(createSession).toHaveBeenCalledWith('user-123', { + workbench: { enable: false }, + }) + expect(values).toHaveBeenCalledWith({ + user_id: 'user-123', + session_id: 'fresh-session', + }) + }) + + test('returns the persisted session when concurrent creation stores a different session', async () => { + createSession = mock(async () => ({ sessionId: 'losing-session', execute })) + useSession = mock(async () => ({ sessionId: 'winning-session', execute })) + const { db, values, onConflictDoNothing } = makeDb([ + null, + 'winning-session', + ]) + + const result = await executeComposioTool({ + db, + userId: 'user-123', + logger, + apiKey: 'test-composio-api-key', + toolName: 'composio_search_tools', + input: { queries: ['gmail'], session: { generate_id: true } }, + }) + + expect(result).toEqual([{ type: 'json', value: { ok: true } }]) + expect(createSession).toHaveBeenCalledWith('user-123', { + workbench: { enable: false }, + }) + expect(values).toHaveBeenCalledWith({ + user_id: 'user-123', + session_id: 'losing-session', + }) + expect(onConflictDoNothing).toHaveBeenCalledTimes(1) + expect(useSession).toHaveBeenCalledWith('winning-session') + expect(execute).toHaveBeenCalledWith('COMPOSIO_SEARCH_TOOLS', { + queries: ['gmail'], + session: { generate_id: true }, + }) + }) + + test('forces multi-execute workbench sync off before calling Composio', async () => { + const { db } = makeDb('stored-session') + + const result = await executeComposioTool({ + db, + userId: 'user-123', + logger, + apiKey: 'test-composio-api-key', + toolName: 'composio_multi_execute_tool', + input: { + tools: [{ slug: 'GMAIL_FETCH_EMAILS', arguments: {} }], + sync_response_to_workbench: true, + }, + }) + + expect(result).toEqual([{ type: 'json', value: { ok: true } }]) + expect(execute).toHaveBeenCalledWith('COMPOSIO_MULTI_EXECUTE_TOOL', { + tools: [{ slug: 'GMAIL_FETCH_EMAILS', arguments: {} }], + sync_response_to_workbench: false, + }) + }) + + test('keeps the stored session row when rehydration fails transiently', async () => { + const transientError = Object.assign(new Error('Composio unavailable'), { + status: 502, + }) + useSession = mock(async () => { + throw transientError + }) + const { db, whereDelete } = makeDb('stored-session') + + await expect( + executeComposioTool({ + db, + userId: 'user-123', + logger, + apiKey: 'test-composio-api-key', + toolName: 'composio_search_tools', + input: { queries: ['gmail'], session: { generate_id: true } }, + }), + ).rejects.toThrow('Composio unavailable') + + expect(whereDelete).not.toHaveBeenCalled() + expect(createSession).not.toHaveBeenCalled() + }) +}) diff --git a/web/src/server/composio-rate-limiter.ts b/web/src/server/composio-rate-limiter.ts new file mode 100644 index 0000000000..8ed8dd8225 --- /dev/null +++ b/web/src/server/composio-rate-limiter.ts @@ -0,0 +1,99 @@ +const SECOND_MS = 1000 +const MINUTE_MS = 60 * SECOND_MS +const HOUR_MS = 60 * MINUTE_MS + +export type ComposioRateLimitResult = + | { limited: false } + | { limited: true; retryAfterMs: number; windowName: string } + +type RateWindow = { + name: string + windowMs: number + maxRequests: number +} + +type WindowTracker = { + count: number + windowStart: number +} + +const RATE_WINDOWS: RateWindow[] = [ + { name: '1 minute', windowMs: MINUTE_MS, maxRequests: 120 }, + { name: '1 hour', windowMs: HOUR_MS, maxRequests: 1_000 }, +] + +const userWindows = new Map>() +let lastCleanupTime = 0 +const CLEANUP_INTERVAL_MS = 5 * MINUTE_MS + +function cleanupExpiredEntries(): void { + const now = Date.now() + for (const [key, windows] of userWindows) { + for (const [windowName, tracker] of windows) { + const matchingWindow = RATE_WINDOWS.find((w) => w.name === windowName) + if ( + !matchingWindow || + now - tracker.windowStart >= matchingWindow.windowMs + ) { + windows.delete(windowName) + } + } + if (windows.size === 0) { + userWindows.delete(key) + } + } +} + +export function checkComposioRateLimit( + userId: string, +): ComposioRateLimitResult { + const now = Date.now() + if (now - lastCleanupTime > CLEANUP_INTERVAL_MS) { + cleanupExpiredEntries() + lastCleanupTime = now + } + + const key = userId + let windows = userWindows.get(key) + if (!windows) { + windows = new Map() + userWindows.set(key, windows) + } + + // First pass checks every window without mutating counters. + for (const rateWindow of RATE_WINDOWS) { + let tracker = windows.get(rateWindow.name) + if (tracker && now - tracker.windowStart >= rateWindow.windowMs) { + windows.delete(rateWindow.name) + tracker = undefined + } + + if ((tracker?.count ?? 0) >= rateWindow.maxRequests) { + return { + limited: true, + windowName: rateWindow.name, + retryAfterMs: Math.max( + 0, + rateWindow.windowMs - (now - tracker!.windowStart), + ), + } + } + } + + // Second pass increments only allowed requests. + for (const rateWindow of RATE_WINDOWS) { + let tracker = windows.get(rateWindow.name) + if (!tracker) { + tracker = { count: 0, windowStart: now } + windows.set(rateWindow.name, tracker) + } + tracker.count++ + } + + return { limited: false } +} + +export function resetComposioRateLimits(): void { + userWindows.clear() + lastCleanupTime = 0 +} diff --git a/web/src/server/composio.ts b/web/src/server/composio.ts new file mode 100644 index 0000000000..af4c5994a2 --- /dev/null +++ b/web/src/server/composio.ts @@ -0,0 +1,346 @@ +import 'server-only' + +import { existsSync, readFileSync } from 'fs' +import { homedir } from 'os' +import path from 'path' + +import { + COMPOSIO_API_KEY_ENV_VAR, + getComposioUpstreamToolName, +} from '@codebuff/common/constants/composio' +import { getErrorObject } from '@codebuff/common/util/error' +import { env } from '@codebuff/internal/env' +import * as schema from '@codebuff/internal/db/schema' +import { Composio } from '@composio/core' +import { and, eq } from 'drizzle-orm' + +import type { Logger } from '@codebuff/common/types/contracts/logger' +import type { JSONValue } from '@codebuff/common/types/json' +import type { ToolResultOutput } from '@codebuff/common/types/messages/content-part' +import type { CodebuffPgDatabase } from '@codebuff/internal/db/types' +import type { ComposioMetaToolName } from '@codebuff/common/constants/composio' + +const COMPOSIO_HOME_ENV_PATH = path.join(homedir(), 'codebuff', '.env.local') + +type ComposioSession = Awaited> +type ComposioClient = Composio + +type CachedComposioSession = { + userId: string + sessionId: string + session: ComposioSession +} + +function parseEnvFileValue(contents: string, key: string): string | undefined { + for (const rawLine of contents.split(/\r?\n/)) { + const line = rawLine.trim() + if (!line || line.startsWith('#')) continue + + const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/) + if (!match || match[1] !== key) continue + + const value = match[2].trim() + return value.replace(/^(['"])(.*)\1$/, '$2') + } + return undefined +} + +function getComposioApiKey(): string | undefined { + const configuredApiKey = env.COMPOSIO_API_KEY?.trim() + if (configuredApiKey) return configuredApiKey + + if (!existsSync(COMPOSIO_HOME_ENV_PATH)) return undefined + + try { + return parseEnvFileValue( + readFileSync(COMPOSIO_HOME_ENV_PATH, 'utf8'), + COMPOSIO_API_KEY_ENV_VAR, + )?.trim() + } catch { + return undefined + } +} + +export function isComposioConfigured(): boolean { + return !!getComposioApiKey() +} + +function toJsonValue(value: unknown): JSONValue { + try { + return JSON.parse(JSON.stringify(value ?? null)) as JSONValue + } catch { + return String(value) as JSONValue + } +} + +function getComposioClient(apiKey: string): ComposioClient { + return new Composio({ + apiKey, + host: 'codebuff', + }) +} + +const COMPOSIO_SESSION_CONFIG = { + workbench: { + enable: false, + }, +} as const + +async function insertSessionIfAbsent(params: { + db: CodebuffPgDatabase + userId: string + sessionId: string +}) { + await params.db + .insert(schema.composioSession) + .values({ + user_id: params.userId, + session_id: params.sessionId, + }) + .onConflictDoNothing({ + target: schema.composioSession.user_id, + }) +} + +async function getStoredSessionByUser(params: { + db: CodebuffPgDatabase + userId: string +}) { + return params.db.query.composioSession.findFirst({ + where: eq(schema.composioSession.user_id, params.userId), + }) +} + +async function deleteStoredSession(params: { + db: CodebuffPgDatabase + userId: string + sessionId: string +}) { + await params.db + .delete(schema.composioSession) + .where( + and( + eq(schema.composioSession.user_id, params.userId), + eq(schema.composioSession.session_id, params.sessionId), + ), + ) +} + +async function createSessionForUser(params: { + db: CodebuffPgDatabase + userId: string + apiKey: string + logger: Logger +}): Promise { + const composio = getComposioClient(params.apiKey) + const session = await composio.create(params.userId, COMPOSIO_SESSION_CONFIG) + await insertSessionIfAbsent({ + db: params.db, + userId: params.userId, + sessionId: session.sessionId, + }) + + const storedSession = await getStoredSessionByUser({ + db: params.db, + userId: params.userId, + }) + if (!storedSession) { + throw new Error('Failed to persist Composio session') + } + + if (storedSession.session_id !== session.sessionId) { + params.logger.info( + { + userId: params.userId, + createdSessionId: session.sessionId, + storedSessionId: storedSession.session_id, + }, + 'Using existing persisted Composio session after concurrent creation', + ) + return rehydrateSession({ + userId: params.userId, + sessionId: storedSession.session_id, + apiKey: params.apiKey, + }) + } + + const cachedSession: CachedComposioSession = { + userId: params.userId, + sessionId: session.sessionId, + session, + } + return cachedSession +} + +function getErrorStatus(error: unknown): number | undefined { + if (!error || typeof error !== 'object') return undefined + + const candidates = [ + 'status', + 'statusCode', + 'code', + 'responseStatus', + 'httpStatus', + ] + for (const key of candidates) { + const value = (error as Record)[key] + if (typeof value === 'number') return value + if (typeof value === 'string' && /^\d+$/.test(value)) { + return Number(value) + } + } + + const response = (error as Record)['response'] + return getErrorStatus(response) +} + +function isInvalidStoredSessionError(error: unknown): boolean { + const status = getErrorStatus(error) + if (status && [400, 401, 403, 404, 410].includes(status)) { + return true + } + + if (!(error instanceof Error)) return false + + const message = error.message.toLowerCase() + return ( + message.includes('session') && + (message.includes('not found') || + message.includes('not exist') || + message.includes('invalid') || + message.includes('expired') || + message.includes('unauthorized') || + message.includes('forbidden')) + ) +} + +async function rehydrateSession(params: { + userId: string + sessionId: string + apiKey: string +}): Promise { + const composio = getComposioClient(params.apiKey) + const session = await composio.use(params.sessionId) + const cachedSession: CachedComposioSession = { + userId: params.userId, + sessionId: params.sessionId, + session, + } + return cachedSession +} + +async function getSessionForUser(params: { + db: CodebuffPgDatabase + userId: string + logger: Logger + apiKey?: string +}): Promise { + const apiKey = params.apiKey ?? getComposioApiKey() + if (!apiKey) return null + + try { + const storedSession = await getStoredSessionByUser({ + db: params.db, + userId: params.userId, + }) + if (storedSession) { + params.logger.info( + { userId: params.userId }, + 'Rehydrating Composio session from database', + ) + try { + return await rehydrateSession({ + userId: params.userId, + sessionId: storedSession.session_id, + apiKey, + }) + } catch (error) { + if (!isInvalidStoredSessionError(error)) { + throw error + } + + params.logger.warn( + { + error: getErrorObject(error), + userId: params.userId, + sessionId: storedSession.session_id, + }, + 'Stored Composio session is invalid; replacing it', + ) + await deleteStoredSession({ + db: params.db, + userId: params.userId, + sessionId: storedSession.session_id, + }) + } + } + + params.logger.info( + { userId: params.userId }, + 'Creating new Composio session', + ) + return createSessionForUser({ + db: params.db, + userId: params.userId, + apiKey, + logger: params.logger, + }) + } catch (error) { + params.logger.error( + { error: getErrorObject(error), userId: params.userId }, + 'Failed to initialize Composio session', + ) + throw error + } +} + +export async function executeComposioTool(params: { + db: CodebuffPgDatabase + userId: string + toolName: ComposioMetaToolName + input: Record + logger: Logger + apiKey?: string +}): Promise { + const apiKey = params.apiKey ?? getComposioApiKey() + if (!apiKey) return null + + const cached = await getSessionForUser({ + db: params.db, + userId: params.userId, + logger: params.logger, + apiKey, + }) + if (!cached) return null + + try { + const input = + params.toolName === 'composio_multi_execute_tool' + ? { + ...params.input, + sync_response_to_workbench: false, + } + : params.input + const upstreamToolName = getComposioUpstreamToolName(params.toolName) + const result = await cached.session.execute(upstreamToolName, input) + return [{ type: 'json', value: toJsonValue(result) }] + } catch (error) { + params.logger.warn( + { + error: getErrorObject(error), + userId: params.userId, + sessionId: cached.sessionId, + toolName: params.toolName, + }, + 'Composio tool execution failed', + ) + return [ + { + type: 'json', + value: { + errorMessage: error instanceof Error ? error.message : String(error), + }, + }, + ] + } +} diff --git a/web/src/test-stubs/bun-test.ts b/web/src/test-stubs/bun-test.ts index 2c1d129de8..a085d6855f 100644 --- a/web/src/test-stubs/bun-test.ts +++ b/web/src/test-stubs/bun-test.ts @@ -1,5 +1,7 @@ import { + afterAll, afterEach, + beforeAll, beforeEach, describe, expect, @@ -29,4 +31,14 @@ mock.module = (moduleName, factory) => { jest.mock(moduleName, factory) } -export { afterEach, beforeEach, describe, expect, it, test, mock } +export { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + test, + mock, +}