From c9d495bb6041cf9f2b8bd3080e940daf582d174a Mon Sep 17 00:00:00 2001 From: Finley Ge Date: Tue, 14 Jul 2026 11:51:33 +0800 Subject: [PATCH 1/3] fix(security): harden system and temporary tool secrets Synchronize toolset secrets from parent records, clear temporary values when switching secret sources, and keep production system secrets out of debug tool flows. Add regression coverage for parent-child authority, debug fail-closed behavior, and app secret normalization. --- .../global/core/app/tool/systemTool/codec.ts | 8 +- packages/service/core/app/controller.ts | 32 ++-- .../app/tool/systemTool/systemTool.repo.ts | 23 ++- .../tool/systemTool/systemTool.repo.test.ts | 175 +++++++++++++++++- .../app/tool/SecretInputModal.tsx | 16 +- .../api/core/plugin/admin/tool/update.ts | 13 +- .../api/core/plugin/admin/tool/update.test.ts | 90 +++++++++ .../test/service/core/app/controller.test.ts | 94 ++++++++++ 8 files changed, 428 insertions(+), 23 deletions(-) create mode 100644 projects/app/test/api/core/plugin/admin/tool/update.test.ts diff --git a/packages/global/core/app/tool/systemTool/codec.ts b/packages/global/core/app/tool/systemTool/codec.ts index b068c8502d1c..d798af6f6dc6 100644 --- a/packages/global/core/app/tool/systemTool/codec.ts +++ b/packages/global/core/app/tool/systemTool/codec.ts @@ -5,6 +5,7 @@ import type { SystemPluginToolCollectionType } from '../../../plugin/tool/type'; import { PluginStatusEnum } from '../../../plugin/type'; import { SystemToolSystemSecretStatusEnum } from './constants'; import type { SystemToolListItemType } from './type'; +import { isDebugToolSource } from '../utils'; type SystemToolConfigLike = SystemPluginToolCollectionType & { toObject?: () => SystemPluginToolCollectionType; @@ -87,13 +88,16 @@ export const SystemToolCodec = { attachToolConfig({ tool, config, - lang + lang, + source }: { tool: ToolListItemType; config?: SystemPluginToolCollectionType; lang?: `${LangEnum}`; + source?: string; }): SystemToolListItemType { - const configuredSecretsVal = this.getConfiguredSecretsVal(config); + const isDebugSource = isDebugToolSource(source) || isDebugToolSource(tool.source); + const configuredSecretsVal = isDebugSource ? undefined : this.getConfiguredSecretsVal(config); const hasSystemSecret = !!configuredSecretsVal; return { diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index d11d3784e196..008917ceb84b 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -79,6 +79,26 @@ export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] } // Format header secret node.inputs.forEach((input) => { + if ( + input.key === NodeInputKeyEnum.systemInputConfig && + typeof input.value === 'object' && + input.value !== null + ) { + if (input.value?.type !== SystemToolSecretInputTypeEnum.manual) { + // system/team 只保存来源类型,禁止把临时密钥值带入应用节点。 + delete input.value.value; + } else { + const secretValues = input.value.value; + if (secretValues && typeof secretValues === 'object' && !Array.isArray(secretValues)) { + input.inputList?.forEach((inputItem) => { + if (inputItem.inputType === 'secret') { + secretValues[inputItem.key] = encryptSecretValue(secretValues[inputItem.key]); + } + }); + } + } + } + if (nodeInputIsReference(input)) return; // 敏感信息 @@ -88,18 +108,6 @@ export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] } if (input.renderTypeList?.includes(FlowNodeInputTypeEnum.password)) { input.value = encryptSecretValue(input.value); } - if (input.key === NodeInputKeyEnum.systemInputConfig && typeof input.value === 'object') { - input.inputList?.forEach((inputItem) => { - if ( - inputItem.inputType === 'secret' && - input.value?.type === SystemToolSecretInputTypeEnum.manual && - input.value?.value - ) { - input.value.value[inputItem.key] = encryptSecretValue(input.value.value[inputItem.key]); - } - }); - } - // 知识库 if (isDatasetNode) { // Agent diff --git a/packages/service/core/app/tool/systemTool/systemTool.repo.ts b/packages/service/core/app/tool/systemTool/systemTool.repo.ts index 817e47925592..4eb516943c85 100644 --- a/packages/service/core/app/tool/systemTool/systemTool.repo.ts +++ b/packages/service/core/app/tool/systemTool/systemTool.repo.ts @@ -314,7 +314,8 @@ export class SystemToolRepo { const item = SystemToolCodec.attachToolConfig({ tool, config: getFirstSystemToolConfig(DBPluginsMap, tool.pluginId), - lang + lang, + source: tool.source }); return { @@ -479,8 +480,16 @@ export class SystemToolRepo { childPluginId ? child!.outputSchema : tool.outputSchema ); const secrets = jsonSchema2SecretInput({ jsonSchema: secretSchema }); - const configuredSecretsVal = SystemToolCodec.getConfiguredSecretsVal(dbTool); - const hasSystemSecret = !!configuredSecretsVal; + const parentDbTool = await getParentSystemToolConfig({ + pluginId, + idSource, + parentPluginId + }); + const secretConfig = parentDbTool ?? dbTool; + const configuredSecretsVal = isDebugSource + ? undefined + : SystemToolCodec.getConfiguredSecretsVal(secretConfig); + const hasSystemSecret = !isDebugSource && !!configuredSecretsVal; const toolDetail: SystemToolDetailType = { id: pluginId, @@ -786,7 +795,9 @@ export class SystemToolRepo { version: tool.version, currentCost: dbTool?.currentCost ?? 0, systemKeyCost: dbTool?.systemKeyCost ?? 0, - secretsVal: isDebugSource ? undefined : SystemToolCodec.getConfiguredSecretsVal(dbTool), + secretsVal: isDebugSource + ? undefined + : SystemToolCodec.getConfiguredSecretsVal(parentDbTool ?? dbTool), permissions: tool.permission }; } @@ -796,7 +807,9 @@ export class SystemToolRepo { version, currentCost: dbTool.currentCost ?? 0, systemKeyCost: dbTool.systemKeyCost ?? 0, - secretsVal: SystemToolCodec.getConfiguredSecretsVal(dbTool) + secretsVal: isDebugSource + ? undefined + : SystemToolCodec.getConfiguredSecretsVal(parentDbTool ?? dbTool) }; }; diff --git a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts index 0ddb3254fcd2..4762573b96b9 100644 --- a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts +++ b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts @@ -215,6 +215,7 @@ describe('SystemToolRepo.getSystemToolList', () => { createPluginTool({ pluginId: 'getTime', name: 'Get Time', + hasSecret: true, source: 'debug:tmbId:tmb-1' }) ]); @@ -223,7 +224,8 @@ describe('SystemToolRepo.getSystemToolList', () => { pluginId: 'getTime', pluginOrder: 1, tags: [], - status: PluginStatusEnum.SoonOffline + status: PluginStatusEnum.SoonOffline, + secretsVal: { apiKey: 'production-secret' } }) ]); @@ -236,6 +238,7 @@ describe('SystemToolRepo.getSystemToolList', () => { source: 'debug:tmbId:tmb-1', status: PluginStatusEnum.Normal }); + expect(tools[0].hasSystemSecret).toBe(false); }); }); @@ -410,6 +413,133 @@ describe('SystemToolRepo.getSystemToolDetail', () => { expect(tool).not.toHaveProperty('secrets'); }); + it('uses the parent tool secret for toolset child details', async () => { + const parentSecrets = { apiKey: 'parent-secret' }; + mocks.findSystemTool + .mockResolvedValueOnce({ + pluginId: 'systemTool-weather/forecast', + currentCost: 1, + systemKeyCost: 2, + secretsVal: { apiKey: 'stale-child-secret' }, + customConfig: {} + }) + .mockResolvedValueOnce({ + pluginId: 'systemTool-weather', + secretsVal: parentSecrets, + customConfig: {} + }); + mocks.findSystemTools.mockResolvedValue([]); + mocks.getTool.mockResolvedValue({ + source: 'system', + isToolset: true, + name: { en: 'Weather' }, + description: { en: 'Weather intro' }, + pluginId: 'weather', + version: '1.0.0', + icon: 'weather.svg', + tags: [], + toolDescription: 'Weather tool', + hasSecret: true, + secretSchema: { + type: 'object', + properties: { apiKey: { type: 'string', isSecret: true } } + }, + children: [ + { + id: 'forecast', + name: { en: 'Forecast' }, + description: { en: 'Forecast intro' }, + toolDescription: 'Forecast tool' + } + ] + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolDetail({ + pluginId: 'systemTool-weather/forecast' + }); + + expect(tool.secretsVal).toEqual(parentSecrets); + expect(tool.hasSystemSecret).toBe(true); + }); + + it('treats an explicit null parent secret as disabled for toolset children', async () => { + mocks.findSystemTool + .mockResolvedValueOnce({ + pluginId: 'systemTool-weather/forecast', + secretsVal: { apiKey: 'stale-child-secret' }, + customConfig: {} + }) + .mockResolvedValueOnce({ + pluginId: 'systemTool-weather', + secretsVal: null, + customConfig: {} + }); + mocks.findSystemTools.mockResolvedValue([]); + mocks.getTool.mockResolvedValue({ + source: 'system', + isToolset: true, + name: { en: 'Weather' }, + description: { en: 'Weather intro' }, + pluginId: 'weather', + version: '1.0.0', + icon: 'weather.svg', + tags: [], + toolDescription: 'Weather tool', + hasSecret: true, + secretSchema: { + type: 'object', + properties: { apiKey: { type: 'string', isSecret: true } } + }, + children: [ + { + id: 'forecast', + name: { en: 'Forecast' }, + description: { en: 'Forecast intro' } + } + ] + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolDetail({ + pluginId: 'systemTool-weather/forecast' + }); + + expect(tool.secretsVal).toBeUndefined(); + expect(tool.hasSystemSecret).toBe(false); + }); + + it('hides configured system secrets from debug details', async () => { + mocks.findSystemTool.mockResolvedValue({ + pluginId: 'systemTool-weather', + secretsVal: { apiKey: 'production-secret' }, + customConfig: {} + }); + mocks.getTool.mockResolvedValue({ + source: 'debug:tmbId:tmb-1', + isToolset: false, + name: { en: 'Weather' }, + description: { en: 'Weather intro' }, + pluginId: 'weather', + version: '1.0.0', + icon: 'weather.svg', + tags: [], + toolDescription: 'Weather tool', + hasSecret: true, + secretSchema: { + type: 'object', + properties: { apiKey: { type: 'string', isSecret: true } } + } + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolDetail({ + pluginId: 'systemTool-weather', + source: 'debug:tmbId:tmb-1' + }); + + expect(tool.hasSystemSecret).toBe(false); + expect(tool.secretsVal).toBeUndefined(); + expect(tool.systemSecretStatus).toBe(SystemToolSystemSecretStatusEnum.unconfigured); + }); + it('omits null schemas returned by plugin client', async () => { mocks.findSystemTool.mockResolvedValue({ pluginId: 'systemTool-perplexity', @@ -1020,6 +1150,49 @@ describe('SystemToolRepo.getSystemToolRuntime', () => { expect(tool.secretsVal).toEqual({ apiKey: 'prod-secret' }); }); + + it('uses the parent system secret for toolset child runtime', async () => { + mocks.findSystemTool + .mockResolvedValueOnce({ + pluginId: 'systemTool-weather/forecast', + secretsVal: { apiKey: 'stale-child-secret' }, + customConfig: {} + }) + .mockResolvedValueOnce({ + pluginId: 'systemTool-weather', + secretsVal: { apiKey: 'parent-secret' }, + customConfig: {} + }); + mocks.getTool.mockResolvedValue({ + pluginId: 'weather', + version: '1.0.0', + permission: [] + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolRuntime({ + pluginId: 'systemTool-weather/forecast', + source: 'system' + }); + + expect(tool.secretsVal).toEqual({ apiKey: 'parent-secret' }); + }); + + it('keeps debug runtime fail-closed for workflow tool secrets', async () => { + mocks.findSystemTool.mockResolvedValue({ + pluginId: 'systemTool-workflow-tool', + secretsVal: { apiKey: 'production-secret' }, + customConfig: { + associatedPluginId: 'app-id' + } + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolRuntime({ + pluginId: 'systemTool-workflow-tool', + source: 'debug:tmbId:tmb-1' + }); + + expect(tool.secretsVal).toBeUndefined(); + }); }); describe('SystemToolRepo.getVersions', () => { diff --git a/projects/app/src/pageComponents/app/tool/SecretInputModal.tsx b/projects/app/src/pageComponents/app/tool/SecretInputModal.tsx index 93eb10ce0ece..d4113c49da5a 100644 --- a/projects/app/src/pageComponents/app/tool/SecretInputModal.tsx +++ b/projects/app/src/pageComponents/app/tool/SecretInputModal.tsx @@ -280,7 +280,14 @@ const SecretInputModal = ({ {t('common:had_auth_value')} - setEditIndex(i)} /> + { + setEditIndex(i); + // 进入编辑态即表示替换该密钥,空提交需要清除旧密文。 + setValue(`value.${item.key}.secret` as any, ''); + }} + /> )} @@ -346,7 +353,12 @@ const SecretInputModal = ({ } ]} value={configType} - onChange={(e) => setValue('type', e)} + onChange={(e) => { + setValue('type', e); + if (e !== SystemToolSecretInputTypeEnum.manual) { + setValue('value', undefined); + } + }} /> diff --git a/projects/app/src/pages/api/core/plugin/admin/tool/update.ts b/projects/app/src/pages/api/core/plugin/admin/tool/update.ts index b516be6e04d1..031420914865 100644 --- a/projects/app/src/pages/api/core/plugin/admin/tool/update.ts +++ b/projects/app/src/pages/api/core/plugin/admin/tool/update.ts @@ -20,7 +20,9 @@ const omitUndefinedFields = >(fields: T) => Object.entries(fields).filter(([, value]) => value !== undefined) ) as Partial; -async function handler( +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +export async function handler( req: ApiRequestProps, _res: ApiResponseType ): Promise { @@ -71,6 +73,15 @@ async function handler( { upsert: true, session } ); + if ('secretsVal' in updateFields) { + // 工具集的系统密钥只由父工具维护,覆盖历史子工具记录,避免子工具残留旧密钥。 + await MongoSystemTool.updateMany( + { pluginId: { $regex: `^${escapeRegExp(pluginId)}/` } }, + { secretsVal: updateFields.secretsVal ?? null }, + { session } + ); + } + // 如果有子工具,更新子工具 for await (const tool of updateFields.children || []) { const childPluginId = tool.id.includes('/') ? tool.id : `${pluginId}/${tool.id}`; diff --git a/projects/app/test/api/core/plugin/admin/tool/update.test.ts b/projects/app/test/api/core/plugin/admin/tool/update.test.ts new file mode 100644 index 000000000000..4217ffce2ed9 --- /dev/null +++ b/projects/app/test/api/core/plugin/admin/tool/update.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authSystemAdmin: vi.fn(), + findOne: vi.fn(), + updateOne: vi.fn(), + updateMany: vi.fn(), + mongoSessionRun: vi.fn() +})); + +vi.mock('@/service/middleware/entry', () => ({ + NextAPI: (handler: any) => handler +})); + +vi.mock('@fastgpt/service/support/permission/user/auth', () => ({ + authSystemAdmin: mocks.authSystemAdmin +})); + +vi.mock('@fastgpt/service/core/plugin/tool/systemToolSchema', () => ({ + MongoSystemTool: { + findOne: mocks.findOne, + updateOne: mocks.updateOne, + updateMany: mocks.updateMany + } +})); + +vi.mock('@fastgpt/service/common/mongo/sessionRun', () => ({ + mongoSessionRun: mocks.mongoSessionRun +})); + +import { handler } from '@/pages/api/core/plugin/admin/tool/update'; + +describe('admin system tool update handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.authSystemAdmin.mockResolvedValue(undefined); + mocks.findOne.mockResolvedValue(undefined); + mocks.updateOne.mockResolvedValue(undefined); + mocks.updateMany.mockResolvedValue(undefined); + mocks.mongoSessionRun.mockImplementation((fn: (session: string) => unknown) => fn('session')); + }); + + it('同步父工具密钥到所有已有子工具,即使请求没有携带 children', async () => { + await handler( + { + body: { + id: 'systemTool-weather', + secretsVal: null + } + } as any, + {} as any + ); + + expect(mocks.updateMany).toHaveBeenCalledWith( + { pluginId: { $regex: '^systemTool-weather/' } }, + { secretsVal: null }, + { session: 'session' } + ); + expect(mocks.updateOne).toHaveBeenCalledTimes(1); + }); + + it('为新建的子工具写入与父工具一致的密钥', async () => { + const secretsVal = { apiKey: 'encrypted-value' }; + + await handler( + { + body: { + id: 'systemTool-weather', + secretsVal, + children: [{ id: 'forecast', systemKeyCost: 2 }] + } + } as any, + {} as any + ); + + expect(mocks.updateMany).toHaveBeenCalledWith( + { pluginId: { $regex: '^systemTool-weather/' } }, + { secretsVal }, + { session: 'session' } + ); + expect(mocks.updateOne).toHaveBeenLastCalledWith( + { pluginId: 'systemTool-weather/forecast' }, + expect.objectContaining({ + pluginId: 'systemTool-weather/forecast', + secretsVal + }), + { upsert: true, session: 'session' } + ); + }); +}); diff --git a/projects/app/test/service/core/app/controller.test.ts b/projects/app/test/service/core/app/controller.test.ts index 3abc5c2f7241..d9359ee36fb8 100644 --- a/projects/app/test/service/core/app/controller.test.ts +++ b/projects/app/test/service/core/app/controller.test.ts @@ -7,6 +7,7 @@ import { FlowNodeInputTypeEnum, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import { MongoAgentSkills } from '@fastgpt/service/core/ai/skill/model/schema'; @@ -16,6 +17,99 @@ import { getUser } from '@test/datas/users'; import { SkillErrEnum } from '@fastgpt/global/common/error/code/skill'; describe('beforeUpdateAppFormat', () => { + it.each([SystemToolSecretInputTypeEnum.system, SystemToolSecretInputTypeEnum.team])( + '保存前清理 %s 类型中的临时密钥值', + (type) => { + const nodes = [ + { + inputs: [ + { + key: NodeInputKeyEnum.systemInputConfig, + value: { + type, + value: { + apiKey: { + value: 'temporary-secret', + secret: '' + } + } + }, + inputList: [{ key: 'apiKey', inputType: 'secret' }] + } + ] + } as StoreNodeItemType + ]; + + beforeUpdateAppFormat({ nodes }); + + expect(nodes[0].inputs[0].value).toEqual({ type }); + } + ); + + it('即使系统输入被标记为引用,也不能保留临时密钥值', () => { + const nodes = [ + { + inputs: [ + { + key: NodeInputKeyEnum.systemInputConfig, + selectedTypeIndex: 0, + renderTypeList: [FlowNodeInputTypeEnum.reference], + value: { + type: SystemToolSecretInputTypeEnum.system, + value: { + apiKey: { + value: 'temporary-secret', + secret: '' + } + } + }, + inputList: [{ key: 'apiKey', inputType: 'secret' }] + } + ] + } as StoreNodeItemType + ]; + + beforeUpdateAppFormat({ nodes }); + + expect(nodes[0].inputs[0].value).toEqual({ + type: SystemToolSecretInputTypeEnum.system + }); + }); + + it('保存前仅加密 manual 类型的临时密钥值', () => { + const nodes = [ + { + inputs: [ + { + key: NodeInputKeyEnum.systemInputConfig, + value: { + type: SystemToolSecretInputTypeEnum.manual, + value: { + apiKey: { + value: 'temporary-secret', + secret: '' + }, + region: 'cn' + } + }, + inputList: [ + { key: 'apiKey', inputType: 'secret' }, + { key: 'region', inputType: 'input' } + ] + } + ] + } as StoreNodeItemType + ]; + + beforeUpdateAppFormat({ nodes }); + + const value = nodes[0].inputs[0].value as any; + expect(value.type).toBe(SystemToolSecretInputTypeEnum.manual); + expect(value.value.apiKey.value).toBe(''); + expect(value.value.apiKey.secret).toEqual(expect.any(String)); + expect(value.value.region).toBe('cn'); + }); + it('保存前统一压缩知识库选择项,去掉编辑态删除标记和快照字段', () => { const nodes = [ { From 77034823e0b0096a3aca718789213a6f57595d3e Mon Sep 17 00:00:00 2001 From: Finley Ge Date: Thu, 16 Jul 2026 11:15:41 +0800 Subject: [PATCH 2/3] fix(security): encrypt system tool secrets Store plugin system secrets as AES-GCM wrappers, mask configured values in administrator responses, and keep legacy plaintext and inputListVal records readable during migration. Update the administrator form to edit write-only markers without sending stored ciphertext back to the server. --- .../core/app/tool/systemTool/constants.ts | 3 + .../core/app/tool/systemTool/secrets.ts | 138 ++++++++++++++++++ .../app/tool/systemTool/systemTool.repo.ts | 25 +++- .../core/app/tool/systemTool/secrets.test.ts | 78 ++++++++++ .../tool/systemTool/systemTool.repo.test.ts | 114 ++++++++++++++- .../config/tool/SystemToolConfigModal.tsx | 40 +++++ .../api/core/plugin/admin/tool/detail.ts | 3 +- .../api/core/plugin/admin/tool/update.ts | 31 +++- .../api/core/plugin/admin/tool/update.test.ts | 52 ++++++- 9 files changed, 471 insertions(+), 13 deletions(-) create mode 100644 packages/service/core/app/tool/systemTool/secrets.ts create mode 100644 packages/service/test/core/app/tool/systemTool/secrets.test.ts diff --git a/packages/global/core/app/tool/systemTool/constants.ts b/packages/global/core/app/tool/systemTool/constants.ts index 3b48b86252c2..0e5246bea515 100644 --- a/packages/global/core/app/tool/systemTool/constants.ts +++ b/packages/global/core/app/tool/systemTool/constants.ts @@ -32,3 +32,6 @@ export enum SystemToolSystemSecretStatusEnum { configured = 'configured', unconfigured = 'unconfigured' } + +/** 管理员配置页使用的 write-only 标记,表示该字段已有系统密钥但不回显密文。 */ +export const SystemToolSecretMaskedValue = '__FASTGPT_SYSTEM_SECRET_MASKED__'; diff --git a/packages/service/core/app/tool/systemTool/secrets.ts b/packages/service/core/app/tool/systemTool/secrets.ts new file mode 100644 index 000000000000..c789c27fe65b --- /dev/null +++ b/packages/service/core/app/tool/systemTool/secrets.ts @@ -0,0 +1,138 @@ +import { SystemToolSecretMaskedValue } from '@fastgpt/global/core/app/tool/systemTool/constants'; +import { type InputConfigType } from '@fastgpt/global/core/workflow/type/io'; +import { decryptSecret, encryptSecret } from '../../../../common/secret/aes256gcm'; + +type SecretsVal = Record; + +type SecretValueLike = { + secret?: unknown; + value?: unknown; +}; + +const isMaskedValue = (value: unknown) => value === SystemToolSecretMaskedValue; + +const isSecretValueLike = (value: unknown): value is SecretValueLike => { + return typeof value === 'object' && value !== null && ('secret' in value || 'value' in value); +}; + +const isConfiguredValue = (value: unknown) => { + if (value === undefined || value === null || value === '') return false; + if (isMaskedValue(value)) return true; + + if (isSecretValueLike(value)) { + return (value.value !== undefined && value.value !== '') || !!value.secret; + } + + return true; +}; + +const isEncryptedValue = (value: unknown): value is { secret: string; value?: string } => { + return isSecretValueLike(value) && typeof value.secret === 'string' && value.secret.length > 0; +}; + +const encryptValue = (value: unknown) => { + if (value === undefined || value === null || value === '') return value; + if (isEncryptedValue(value)) return value; + + if (isSecretValueLike(value)) { + if (typeof value.value !== 'string' || value.value === '') return ''; + return { + secret: encryptSecret(value.value), + value: '' + }; + } + + return { + secret: encryptSecret(String(value)), + value: '' + }; +}; + +const decryptValue = (value: unknown) => { + if (!isSecretValueLike(value)) return value; + + if (typeof value.value === 'string' && value.value !== '') return value.value; + if (!isEncryptedValue(value)) return value.value ?? ''; + + try { + return decryptSecret(value.secret); + } catch { + // 兼容历史非 AES 密钥结构,避免一次坏字段导致整个工具无法运行。 + return value.value ?? ''; + } +}; + +/** 从插件 secret schema 生成需要加密的字段集合。 */ +export const getSystemToolSecretKeys = (inputList?: InputConfigType[]) => + new Set((inputList ?? []).filter((item) => item.inputType === 'secret').map((item) => item.key)); + +/** 将管理员提交的系统密钥字段加密,并保留未编辑的已有字段。 */ +export const encryptSystemToolSecrets = ({ + secretsVal, + existingSecretsVal, + secretKeys +}: { + secretsVal?: SecretsVal | null; + existingSecretsVal?: SecretsVal; + secretKeys: Set; +}) => { + if (secretsVal === null || secretsVal === undefined) return secretsVal; + + const result: SecretsVal = { ...(existingSecretsVal ?? {}) }; + + Object.entries(secretsVal).forEach(([key, value]) => { + if (!secretKeys.has(key)) { + result[key] = value; + return; + } + + if (isMaskedValue(value)) { + const existingValue = existingSecretsVal?.[key]; + if (isConfiguredValue(existingValue)) { + result[key] = encryptValue(existingValue); + } else { + delete result[key]; + } + return; + } + + if (value === undefined || value === null || value === '') { + delete result[key]; + return; + } + + result[key] = encryptValue(value); + }); + + return result; +}; + +/** 管理员详情只返回已配置标记,禁止把系统密钥明文或密文回传给前端。 */ +export const maskSystemToolSecrets = ({ + secretsVal, + secretKeys +}: { + secretsVal?: SecretsVal; + secretKeys: Set; +}) => { + if (!secretsVal) return secretsVal; + + return Object.fromEntries( + Object.entries(secretsVal).map(([key, value]) => [ + key, + secretKeys.has(key) && isConfiguredValue(value) ? SystemToolSecretMaskedValue : value + ]) + ); +}; + +/** runtime 使用的系统密钥值,兼容旧明文并解密新格式。 */ +export const decryptSystemToolSecrets = (secretsVal?: SecretsVal) => { + if (!secretsVal) return secretsVal; + + return Object.fromEntries( + Object.entries(secretsVal).flatMap(([key, value]) => { + if (isMaskedValue(value)) return []; + return [[key, decryptValue(value)]]; + }) + ); +}; diff --git a/packages/service/core/app/tool/systemTool/systemTool.repo.ts b/packages/service/core/app/tool/systemTool/systemTool.repo.ts index 4eb516943c85..4c05dedc48f4 100644 --- a/packages/service/core/app/tool/systemTool/systemTool.repo.ts +++ b/packages/service/core/app/tool/systemTool/systemTool.repo.ts @@ -39,6 +39,11 @@ import type { AppToolRuntimeType } from '@fastgpt/global/core/app/tool/type'; import type { PluginPermissionEnumType } from '@fastgpt/global/sdk/fastgpt-plugin'; import { Types } from '../../../../common/mongo'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; +import { + decryptSystemToolSecrets, + getSystemToolSecretKeys, + maskSystemToolSecrets +} from './secrets'; type SystemToolRuntimeType = { id: string; @@ -346,13 +351,15 @@ export class SystemToolRepo { version, source: toolSource = 'system', lang, - fallbackLatestVersion = false + fallbackLatestVersion = false, + maskSecrets = false }: { pluginId: string; version?: string; source?: string; lang?: `${LangEnum}`; fallbackLatestVersion?: boolean; + maskSecrets?: boolean; }): Promise => { const isDebugSource = isDebugToolSource(toolSource); const { pluginId: rawPluginId, source: idSource } = parseSystemToolId({ @@ -480,6 +487,7 @@ export class SystemToolRepo { childPluginId ? child!.outputSchema : tool.outputSchema ); const secrets = jsonSchema2SecretInput({ jsonSchema: secretSchema }); + const secretKeys = getSystemToolSecretKeys(secrets); const parentDbTool = await getParentSystemToolConfig({ pluginId, idSource, @@ -490,6 +498,13 @@ export class SystemToolRepo { ? undefined : SystemToolCodec.getConfiguredSecretsVal(secretConfig); const hasSystemSecret = !isDebugSource && !!configuredSecretsVal; + const visibleSecretsVal = + maskSecrets && !isDebugSource + ? maskSystemToolSecrets({ + secretsVal: configuredSecretsVal, + secretKeys + }) + : configuredSecretsVal; const toolDetail: SystemToolDetailType = { id: pluginId, @@ -501,7 +516,7 @@ export class SystemToolRepo { hasSecret: !!secrets?.length, hasSystemSecret }), - secretsVal: configuredSecretsVal, + secretsVal: visibleSecretsVal, hasTokenFee: dbTool?.hasTokenFee ?? false, intro: dbTool?.customConfig?.intro ?? @@ -797,7 +812,9 @@ export class SystemToolRepo { systemKeyCost: dbTool?.systemKeyCost ?? 0, secretsVal: isDebugSource ? undefined - : SystemToolCodec.getConfiguredSecretsVal(parentDbTool ?? dbTool), + : decryptSystemToolSecrets( + SystemToolCodec.getConfiguredSecretsVal(parentDbTool ?? dbTool) + ), permissions: tool.permission }; } @@ -809,7 +826,7 @@ export class SystemToolRepo { systemKeyCost: dbTool.systemKeyCost ?? 0, secretsVal: isDebugSource ? undefined - : SystemToolCodec.getConfiguredSecretsVal(parentDbTool ?? dbTool) + : decryptSystemToolSecrets(SystemToolCodec.getConfiguredSecretsVal(parentDbTool ?? dbTool)) }; }; diff --git a/packages/service/test/core/app/tool/systemTool/secrets.test.ts b/packages/service/test/core/app/tool/systemTool/secrets.test.ts new file mode 100644 index 000000000000..9ef12527eacb --- /dev/null +++ b/packages/service/test/core/app/tool/systemTool/secrets.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; +import { SystemToolSecretMaskedValue } from '@fastgpt/global/core/app/tool/systemTool/constants'; +import { decryptSecret, encryptSecret } from '@fastgpt/service/common/secret/aes256gcm'; +import { + decryptSystemToolSecrets, + encryptSystemToolSecrets, + getSystemToolSecretKeys, + maskSystemToolSecrets +} from '@fastgpt/service/core/app/tool/systemTool/secrets'; + +const secretKeys = new Set(['apiKey']); + +describe('system tool secrets', () => { + it('encrypts new secret values and leaves non-secret config values unchanged', () => { + const stored = encryptSystemToolSecrets({ + secretsVal: { apiKey: 'new-api-key', region: 'us' }, + secretKeys + }); + + expect(stored).toMatchObject({ + apiKey: { value: '' }, + region: 'us' + }); + expect(stored.apiKey).not.toBe('new-api-key'); + expect(decryptSystemToolSecrets(stored)).toEqual({ + apiKey: 'new-api-key', + region: 'us' + }); + }); + + it('preserves an unedited masked value and migrates legacy plaintext', () => { + const stored = encryptSystemToolSecrets({ + secretsVal: { apiKey: SystemToolSecretMaskedValue }, + existingSecretsVal: { apiKey: 'legacy-api-key' }, + secretKeys + }); + + expect(stored.apiKey).toMatchObject({ value: '' }); + expect(decryptSecret((stored.apiKey as { secret: string }).secret)).toBe('legacy-api-key'); + }); + + it('supports legacy secret wrappers and clears an edited empty value', () => { + const stored = encryptSystemToolSecrets({ + secretsVal: { apiKey: '' }, + existingSecretsVal: { apiKey: { value: 'legacy-api-key', secret: '' } }, + secretKeys + }); + + expect(stored).toEqual({}); + expect(decryptSystemToolSecrets({ apiKey: { value: 'legacy-api-key', secret: '' } })).toEqual({ + apiKey: 'legacy-api-key' + }); + }); + + it('masks only configured secret schema fields', () => { + const masked = maskSystemToolSecrets({ + secretsVal: { + apiKey: { secret: encryptSecret('api-key'), value: '' }, + region: 'us' + }, + secretKeys + }); + + expect(masked).toEqual({ + apiKey: SystemToolSecretMaskedValue, + region: 'us' + }); + }); + + it('derives secret keys from secret input schema entries', () => { + expect( + getSystemToolSecretKeys([ + { key: 'apiKey', label: 'API key', inputType: 'secret' }, + { key: 'region', label: 'Region', inputType: 'select' } + ]) + ).toEqual(new Set(['apiKey'])); + }); +}); diff --git a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts index 4762573b96b9..fdfe4d26f171 100644 --- a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts +++ b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { SystemToolSystemSecretStatusEnum } from '@fastgpt/global/core/app/tool/systemTool/constants'; +import { + SystemToolSecretMaskedValue, + SystemToolSystemSecretStatusEnum +} from '@fastgpt/global/core/app/tool/systemTool/constants'; import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeInputTypeEnum, @@ -7,6 +10,7 @@ import { } from '@fastgpt/global/core/workflow/node/constant'; import { PluginStatusEnum, type PluginStatusType } from '@fastgpt/global/core/plugin/type'; import { PluginErrEnum } from '@fastgpt/global/common/error/code/plugin'; +import { encryptSecret } from '@fastgpt/service/common/secret/aes256gcm'; const mocks = vi.hoisted(() => ({ listTools: vi.fn(), @@ -462,6 +466,70 @@ describe('SystemToolRepo.getSystemToolDetail', () => { expect(tool.hasSystemSecret).toBe(true); }); + it('masks configured system secrets for administrator details', async () => { + mocks.findSystemTool.mockResolvedValue({ + pluginId: 'systemTool-weather', + secretsVal: { apiKey: { secret: encryptSecret('production-secret'), value: '' } }, + customConfig: {} + }); + mocks.findSystemTools.mockResolvedValue([]); + mocks.getTool.mockResolvedValue({ + source: 'system', + isToolset: false, + name: { en: 'Weather' }, + description: { en: 'Weather intro' }, + pluginId: 'weather', + version: '1.0.0', + icon: 'weather.svg', + tags: [], + toolDescription: 'Weather tool', + hasSecret: true, + secretSchema: { + type: 'object', + properties: { apiKey: { type: 'string', isSecret: true } } + } + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolDetail({ + pluginId: 'systemTool-weather', + maskSecrets: true + }); + + expect(tool.secretsVal).toEqual({ apiKey: SystemToolSecretMaskedValue }); + }); + + it('masks legacy inputListVal values without exposing the plaintext', async () => { + mocks.findSystemTool.mockResolvedValue({ + pluginId: 'systemTool-weather', + inputListVal: { apiKey: 'legacy-secret' }, + customConfig: {} + }); + mocks.findSystemTools.mockResolvedValue([]); + mocks.getTool.mockResolvedValue({ + source: 'system', + isToolset: false, + name: { en: 'Weather' }, + description: { en: 'Weather intro' }, + pluginId: 'weather', + version: '1.0.0', + icon: 'weather.svg', + tags: [], + toolDescription: 'Weather tool', + hasSecret: true, + secretSchema: { + type: 'object', + properties: { apiKey: { type: 'string', isSecret: true } } + } + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolDetail({ + pluginId: 'systemTool-weather', + maskSecrets: true + }); + + expect(tool.secretsVal).toEqual({ apiKey: SystemToolSecretMaskedValue }); + }); + it('treats an explicit null parent secret as disabled for toolset children', async () => { mocks.findSystemTool .mockResolvedValueOnce({ @@ -1151,6 +1219,50 @@ describe('SystemToolRepo.getSystemToolRuntime', () => { expect(tool.secretsVal).toEqual({ apiKey: 'prod-secret' }); }); + it('decrypts encrypted system secrets before production runtime', async () => { + mocks.findSystemTool.mockResolvedValue({ + pluginId: 'systemTool-weather', + currentCost: 1, + systemKeyCost: 2, + secretsVal: { apiKey: { secret: encryptSecret('encrypted-prod-secret'), value: '' } }, + customConfig: {} + }); + mocks.getTool.mockResolvedValue({ + pluginId: 'weather', + version: '1.0.0', + permission: [] + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolRuntime({ + pluginId: 'systemTool-weather', + source: 'system' + }); + + expect(tool.secretsVal).toEqual({ apiKey: 'encrypted-prod-secret' }); + }); + + it('reads legacy inputListVal values for production runtime', async () => { + mocks.findSystemTool.mockResolvedValue({ + pluginId: 'systemTool-weather', + currentCost: 1, + systemKeyCost: 2, + inputListVal: { apiKey: 'legacy-prod-secret' }, + customConfig: {} + }); + mocks.getTool.mockResolvedValue({ + pluginId: 'weather', + version: '1.0.0', + permission: [] + }); + + const tool = await SystemToolRepo.getInstance().getSystemToolRuntime({ + pluginId: 'systemTool-weather', + source: 'system' + }); + + expect(tool.secretsVal).toEqual({ apiKey: 'legacy-prod-secret' }); + }); + it('uses the parent system secret for toolset child runtime', async () => { mocks.findSystemTool .mockResolvedValueOnce({ diff --git a/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx b/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx index 2f3485834aca..6ec1e9b02838 100644 --- a/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx +++ b/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx @@ -54,6 +54,8 @@ import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { jsonSchema2SecretInput } from '@fastgpt/global/core/app/jsonschema'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import { SystemToolSecretMaskedValue } from '@fastgpt/global/core/app/tool/systemTool/constants'; +import IconButton from '@/pageComponents/account/team/OrgManage/IconButton'; const COST_LIMITS = { max: 1000, min: 0, step: 0.1 }; const FORM_LABEL_WIDTH = '160px'; @@ -455,6 +457,43 @@ const SystemToolConfigModal = ({ ); + if (item.inputType === 'secret' && fieldValue === SystemToolSecretMaskedValue) { + return ( + + + + + + {t('common:had_auth_value')} + + + {!isToolOffline && ( + { + setValue(`secretsVal.${item.key}`, '', { + shouldDirty: true, + shouldValidate: true + }); + }} + /> + )} + + + ); + } + if (item.inputType === 'switch') { return ( @@ -491,6 +530,7 @@ const SystemToolConfigModal = ({ h={9} borderColor={'myGray.200'} isDisabled={isToolOffline} + type={item.inputType === 'secret' ? 'password' : 'text'} {...register(`secretsVal.${item.key}`, { required: item.required })} diff --git a/projects/app/src/pages/api/core/plugin/admin/tool/detail.ts b/projects/app/src/pages/api/core/plugin/admin/tool/detail.ts index 3cfacaf7a736..f05d1b6c5a7d 100644 --- a/projects/app/src/pages/api/core/plugin/admin/tool/detail.ts +++ b/projects/app/src/pages/api/core/plugin/admin/tool/detail.ts @@ -30,7 +30,8 @@ async function handler( pluginId: toolId, lang, source: 'system', - version + version, + maskSecrets: true }); return AdminSystemToolDetailSchema.parse({ diff --git a/projects/app/src/pages/api/core/plugin/admin/tool/update.ts b/projects/app/src/pages/api/core/plugin/admin/tool/update.ts index 031420914865..1a30cc204e56 100644 --- a/projects/app/src/pages/api/core/plugin/admin/tool/update.ts +++ b/projects/app/src/pages/api/core/plugin/admin/tool/update.ts @@ -8,6 +8,13 @@ import { type UpdateSystemToolBodyType } from '@fastgpt/global/openapi/core/plugin/admin/tool/api'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; +import { jsonSchema2SecretInput } from '@fastgpt/global/core/app/jsonschema'; +import { SystemToolCodec } from '@fastgpt/global/core/app/tool/systemTool/codec'; +import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo'; +import { + encryptSystemToolSecrets, + getSystemToolSecretKeys +} from '@fastgpt/service/core/app/tool/systemTool/secrets'; export type updateToolQuery = Record; @@ -40,6 +47,24 @@ export async function handler( return Promise.reject('Workflow tool should be updated through app update api'); } + const storedSecretsVal = await (async () => { + if (!('secretsVal' in updateFields) || updateFields.secretsVal === null) { + return updateFields.secretsVal; + } + + const toolDetail = await SystemToolRepo.getInstance().getSystemToolDetail({ + pluginId, + source: 'system' + }); + const inputList = jsonSchema2SecretInput({ jsonSchema: toolDetail.secretSchema }); + + return encryptSystemToolSecrets({ + secretsVal: updateFields.secretsVal, + existingSecretsVal: SystemToolCodec.getConfiguredSecretsVal(plugin), + secretKeys: getSystemToolSecretKeys(inputList) + }); + })(); + // 基础更新字段 const baseUpdateFields = omitUndefinedFields({ pluginId, @@ -53,7 +78,7 @@ export async function handler( }); if ('secretsVal' in updateFields) { Object.assign(baseUpdateFields, { - secretsVal: updateFields.secretsVal ?? null + secretsVal: storedSecretsVal ?? null }); } @@ -77,7 +102,7 @@ export async function handler( // 工具集的系统密钥只由父工具维护,覆盖历史子工具记录,避免子工具残留旧密钥。 await MongoSystemTool.updateMany( { pluginId: { $regex: `^${escapeRegExp(pluginId)}/` } }, - { secretsVal: updateFields.secretsVal ?? null }, + { secretsVal: storedSecretsVal ?? null }, { session } ); } @@ -97,7 +122,7 @@ export async function handler( }); if ('secretsVal' in updateFields) { Object.assign(childUpdateFields, { - secretsVal: updateFields.secretsVal + secretsVal: storedSecretsVal }); } diff --git a/projects/app/test/api/core/plugin/admin/tool/update.test.ts b/projects/app/test/api/core/plugin/admin/tool/update.test.ts index 4217ffce2ed9..e65d6bb5b1b3 100644 --- a/projects/app/test/api/core/plugin/admin/tool/update.test.ts +++ b/projects/app/test/api/core/plugin/admin/tool/update.test.ts @@ -1,11 +1,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { SystemToolSecretMaskedValue } from '@fastgpt/global/core/app/tool/systemTool/constants'; +import { decryptSecret } from '@fastgpt/service/common/secret/aes256gcm'; const mocks = vi.hoisted(() => ({ authSystemAdmin: vi.fn(), findOne: vi.fn(), updateOne: vi.fn(), updateMany: vi.fn(), - mongoSessionRun: vi.fn() + mongoSessionRun: vi.fn(), + getSystemToolDetail: vi.fn() })); vi.mock('@/service/middleware/entry', () => ({ @@ -28,6 +31,14 @@ vi.mock('@fastgpt/service/common/mongo/sessionRun', () => ({ mongoSessionRun: mocks.mongoSessionRun })); +vi.mock('@fastgpt/service/core/app/tool/systemTool/systemTool.repo', () => ({ + SystemToolRepo: { + getInstance: () => ({ + getSystemToolDetail: mocks.getSystemToolDetail + }) + } +})); + import { handler } from '@/pages/api/core/plugin/admin/tool/update'; describe('admin system tool update handler', () => { @@ -37,6 +48,14 @@ describe('admin system tool update handler', () => { mocks.findOne.mockResolvedValue(undefined); mocks.updateOne.mockResolvedValue(undefined); mocks.updateMany.mockResolvedValue(undefined); + mocks.getSystemToolDetail.mockResolvedValue({ + secretSchema: { + type: 'object', + properties: { + apiKey: { type: 'string', isSecret: true } + } + } + }); mocks.mongoSessionRun.mockImplementation((fn: (session: string) => unknown) => fn('session')); }); @@ -60,7 +79,7 @@ describe('admin system tool update handler', () => { }); it('为新建的子工具写入与父工具一致的密钥', async () => { - const secretsVal = { apiKey: 'encrypted-value' }; + const secretsVal = { apiKey: 'plain-value' }; await handler( { @@ -73,18 +92,43 @@ describe('admin system tool update handler', () => { {} as any ); + const storedSecretsVal = mocks.updateOne.mock.calls[0][1].secretsVal; + expect(decryptSecret(storedSecretsVal.apiKey.secret)).toBe('plain-value'); + expect(storedSecretsVal.apiKey.value).toBe(''); + expect(mocks.updateMany).toHaveBeenCalledWith( { pluginId: { $regex: '^systemTool-weather/' } }, - { secretsVal }, + { secretsVal: storedSecretsVal }, { session: 'session' } ); expect(mocks.updateOne).toHaveBeenLastCalledWith( { pluginId: 'systemTool-weather/forecast' }, expect.objectContaining({ pluginId: 'systemTool-weather/forecast', - secretsVal + secretsVal: storedSecretsVal }), { upsert: true, session: 'session' } ); }); + + it('保留管理员详情返回的 masked 系统密钥', async () => { + mocks.findOne.mockResolvedValue({ + pluginId: 'systemTool-weather', + secretsVal: { apiKey: 'legacy-value' }, + customConfig: {} + }); + + await handler( + { + body: { + id: 'systemTool-weather', + secretsVal: { apiKey: SystemToolSecretMaskedValue } + } + } as any, + {} as any + ); + + const storedSecretsVal = mocks.updateOne.mock.calls[0][1].secretsVal; + expect(decryptSecret(storedSecretsVal.apiKey.secret)).toBe('legacy-value'); + }); }); From 6fb5061ae68d33bd2193176f69a5d300cc4e747f Mon Sep 17 00:00:00 2001 From: Finley Ge Date: Thu, 16 Jul 2026 16:03:54 +0800 Subject: [PATCH 3/3] fix(ui): use plain input for system secrets Keep system secret values visible while editing and continue to replace saved values with the write-only configured marker after reload. --- .../app/src/pageComponents/config/tool/SystemToolConfigModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx b/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx index 6ec1e9b02838..a0c87b6f2adb 100644 --- a/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx +++ b/projects/app/src/pageComponents/config/tool/SystemToolConfigModal.tsx @@ -530,7 +530,6 @@ const SystemToolConfigModal = ({ h={9} borderColor={'myGray.200'} isDisabled={isToolOffline} - type={item.inputType === 'secret' ? 'password' : 'text'} {...register(`secretsVal.${item.key}`, { required: item.required })}