From 8daf6cf6cbba1246f98a59ce474b6bd633646f46 Mon Sep 17 00:00:00 2001 From: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:40:11 +0530 Subject: [PATCH] feat: captain custom tools v1 (#13890) # Pull Request Template ## Description Adds custom tool support to v1 ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. CleanShot 2026-03-24 at 11 37 33@2x CleanShot 2026-03-24 at 11 38 18@2x CleanShot 2026-03-24 at 11 38
32@2x ## Checklist: - [x] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Shivam Mishra --- .../dashboard/api/captain/customTools.js | 6 ++ .../customTool/CustomToolCard.vue | 9 +- .../customTool/CustomToolForm.vue | 86 +++++++++++++++++-- .../emptyStates/CustomToolsPageEmptyState.vue | 12 +++ .../components-next/sidebar/Sidebar.vue | 30 +++++-- app/javascript/dashboard/featureFlags.js | 1 + .../i18n/locale/en/integrations.json | 10 ++- .../dashboard/captain/captain.routes.js | 2 +- .../routes/dashboard/captain/tools/Index.vue | 22 +++-- config/locales/en.yml | 1 + config/routes.rb | 4 +- .../captain/custom_tools_controller.rb | 24 +++++- enterprise/app/models/captain/custom_tool.rb | 27 ++++-- enterprise/app/models/concerns/toolable.rb | 31 ++++--- .../policies/captain/custom_tool_policy.rb | 4 + .../captain/llm/assistant_chat_service.rb | 22 ++++- .../captain/llm/system_prompts_service.rb | 15 +++- .../captain/tools/custom_http_tool.rb | 47 ++++++++++ .../reconcile_plan_features_service.rb | 2 +- .../models/captain/_custom_tool.json.jbuilder | 2 +- .../captain/custom_tools_controller_spec.rb | 7 +- 21 files changed, 307 insertions(+), 57 deletions(-) create mode 100644 enterprise/app/services/captain/tools/custom_http_tool.rb diff --git a/app/javascript/dashboard/api/captain/customTools.js b/app/javascript/dashboard/api/captain/customTools.js index d0818d941..471c2846b 100644 --- a/app/javascript/dashboard/api/captain/customTools.js +++ b/app/javascript/dashboard/api/captain/customTools.js @@ -31,6 +31,12 @@ class CaptainCustomTools extends ApiClient { delete(id) { return axios.delete(`${this.url}/${id}`); } + + test(data = {}) { + return axios.post(`${this.url}/test`, { + custom_tool: data, + }); + } } export default new CaptainCustomTools(); diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue index d1d1dd011..d5f1e3e52 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CustomToolCard.vue @@ -101,12 +101,9 @@ const authTypeLabel = computed(() => { -
-
- +
+
+ {{ description }} -import { reactive, computed, useTemplateRef, watch } from 'vue'; +import { reactive, computed, ref, useTemplateRef, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useVuelidate } from '@vuelidate/core'; -import { required } from '@vuelidate/validators'; +import { required, maxLength } from '@vuelidate/validators'; import { useMapGetter } from 'dashboard/composables/store'; +import CustomToolsAPI from 'dashboard/api/captain/customTools'; import Input from 'dashboard/components-next/input/Input.vue'; import TextArea from 'dashboard/components-next/textarea/TextArea.vue'; @@ -72,8 +73,12 @@ const DEFAULT_PARAM = { required: false, }; +// OpenAI enforces a 64-char limit on function names. The backend slug is +// "custom_" (7 chars) + parameterized title, so cap the title conservatively. +const MAX_TOOL_NAME_LENGTH = 55; + const validationRules = { - title: { required }, + title: { required, maxLength: maxLength(MAX_TOOL_NAME_LENGTH) }, endpoint_url: { required }, http_method: { required }, auth_type: { required }, @@ -103,9 +108,15 @@ const isLoading = computed(() => ); const getErrorMessage = (field, errorKey) => { - return v$.value[field].$error - ? t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`) - : ''; + if (!v$.value[field].$error) return ''; + + const failedRule = v$.value[field].$errors[0]?.$validator; + if (failedRule === 'maxLength') { + return t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.MAX_LENGTH_ERROR`, { + max: MAX_TOOL_NAME_LENGTH, + }); + } + return t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`); }; const formErrors = computed(() => ({ @@ -140,6 +151,30 @@ const handleSubmit = async () => { emit('submit', state); }; + +const isTesting = ref(false); +const testResult = ref(null); +const isTestDisabled = computed( + () => state.endpoint_url.includes('{{') || !!state.request_template +); + +const handleTest = async () => { + if (!state.endpoint_url) return; + + isTesting.value = true; + testResult.value = null; + try { + const { data } = await CustomToolsAPI.test(state); + const isOk = data.status >= 200 && data.status < 300; + testResult.value = { success: isOk, status: data.status }; + } catch (e) { + const message = + e.response?.data?.error || t('CAPTAIN.CUSTOM_TOOLS.TEST.ERROR'); + testResult.value = { success: false, message }; + } finally { + isTesting.value = false; + } +};