mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-29 18:47:25 +08:00
fix(security): require explicit trust for internal tool routes (#7089)
* fix: require explicit trust for internal tool routes * fix(tools): harden internal route validation * fix(tools): close request audit gaps * fix(tools): validate request trust policies
This commit is contained in:
@@ -51,6 +51,7 @@ export const agiloftAsyncStatusTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/async_status',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -59,6 +59,7 @@ export const agiloftAttachmentInfoTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/attachment_info',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -49,6 +49,7 @@ export const agiloftCreateRecordTool: ToolConfig<AgiloftCreateRecordParams, Agil
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/create_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -62,6 +62,7 @@ export const agiloftDeleteRecordTool: ToolConfig<AgiloftDeleteRecordParams, Agil
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/delete_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -60,6 +60,7 @@ export const agiloftGetChoiceLineIdTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/get_choice_line_id',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -56,6 +56,7 @@ export const agiloftListTablesTool: ToolConfig<AgiloftListTablesParams, AgiloftL
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/list_tables',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -59,6 +59,7 @@ export const agiloftLockRecordTool: ToolConfig<AgiloftLockRecordParams, AgiloftL
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/lock_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -62,6 +62,7 @@ export const agiloftNlpSearchTool: ToolConfig<AgiloftNlpSearchParams, AgiloftNlp
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/nlp_search',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -53,6 +53,7 @@ export const agiloftReadRecordTool: ToolConfig<AgiloftReadRecordParams, AgiloftR
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/read_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -65,6 +65,7 @@ export const agiloftRemoveAttachmentTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/remove_attachment',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -60,6 +60,7 @@ export const agiloftRunActionButtonTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/run_action_button',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -44,6 +44,7 @@ export const agiloftSavedSearchTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/saved_search',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -77,6 +77,7 @@ export const agiloftSearchRecordsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/search_records',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -51,6 +51,7 @@ export const agiloftSelectRecordsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/select_records',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -55,6 +55,7 @@ export const agiloftUpdateRecordTool: ToolConfig<AgiloftUpdateRecordParams, Agil
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/update_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -66,6 +66,7 @@ export const agiloftUpsertRecordTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/agiloft/upsert_record',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -75,6 +75,7 @@ export const confluenceAddLabelTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/labels',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceAddLabelParams) => ({
|
||||
|
||||
@@ -91,6 +91,7 @@ export const confluenceCreateBlogPostTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/blogposts',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceCreateBlogPostParams) => ({
|
||||
|
||||
@@ -66,6 +66,7 @@ export const confluenceCreateCommentTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/comments',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceCreateCommentParams) => {
|
||||
|
||||
@@ -87,6 +87,7 @@ export const confluenceCreatePageTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/create-page',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceCreatePageParams) => {
|
||||
|
||||
@@ -79,6 +79,7 @@ export const confluenceCreatePagePropertyTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/page-properties',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceCreatePagePropertyParams) => ({
|
||||
|
||||
@@ -80,6 +80,7 @@ export const confluenceCreateSpaceTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceCreateSpaceParams) => ({
|
||||
|
||||
@@ -76,6 +76,7 @@ export const confluenceCreateSpacePropertyTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space-properties',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceCreateSpacePropertyParams) => ({
|
||||
|
||||
@@ -59,6 +59,7 @@ export const confluenceDeleteAttachmentTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/attachment',
|
||||
method: 'DELETE',
|
||||
headers: (params: ConfluenceDeleteAttachmentParams) => {
|
||||
|
||||
@@ -60,6 +60,7 @@ export const confluenceDeleteBlogPostTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/blogposts',
|
||||
method: 'DELETE',
|
||||
headers: (params: ConfluenceDeleteBlogPostParams) => ({
|
||||
|
||||
@@ -59,6 +59,7 @@ export const confluenceDeleteCommentTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/comment',
|
||||
method: 'DELETE',
|
||||
headers: (params: ConfluenceDeleteCommentParams) => {
|
||||
|
||||
@@ -68,6 +68,7 @@ export const confluenceDeleteLabelTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/labels',
|
||||
method: 'DELETE',
|
||||
headers: (params: ConfluenceDeleteLabelParams) => ({
|
||||
|
||||
@@ -68,6 +68,7 @@ export const confluenceDeletePageTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceDeletePageParams) => '/api/tools/confluence/page',
|
||||
method: 'DELETE',
|
||||
headers: (params: ConfluenceDeletePageParams) => {
|
||||
|
||||
@@ -68,6 +68,7 @@ export const confluenceDeletePagePropertyTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/page-properties',
|
||||
method: 'DELETE',
|
||||
headers: (params: ConfluenceDeletePagePropertyParams) => ({
|
||||
|
||||
@@ -62,6 +62,7 @@ export const confluenceDeleteSpaceTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space',
|
||||
method: 'DELETE',
|
||||
headers: (params: ConfluenceDeleteSpaceParams) => ({
|
||||
|
||||
@@ -68,6 +68,7 @@ export const confluenceDeleteSpacePropertyTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space-properties',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceDeleteSpacePropertyParams) => ({
|
||||
|
||||
@@ -84,6 +84,7 @@ export const confluenceGetBlogPostTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/blogposts',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetBlogPostParams) => ({
|
||||
|
||||
@@ -74,6 +74,7 @@ export const confluenceGetPageAncestorsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/page-ancestors',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetPageAncestorsParams) => ({
|
||||
|
||||
@@ -83,6 +83,7 @@ export const confluenceGetPageChildrenTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/page-children',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetPageChildrenParams) => ({
|
||||
|
||||
@@ -84,6 +84,7 @@ export const confluenceGetPageDescendantsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/page-descendants',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetPageDescendantsParams) => ({
|
||||
|
||||
@@ -89,6 +89,7 @@ export const confluenceGetPageVersionTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/page-versions',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetPageVersionParams) => ({
|
||||
|
||||
@@ -88,6 +88,7 @@ export const confluenceGetPagesByLabelTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceGetPagesByLabelParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -71,6 +71,7 @@ export const confluenceGetSpaceTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceGetSpaceParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -70,6 +70,7 @@ export const confluenceGetTaskTool: ToolConfig<ConfluenceGetTaskParams, Confluen
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/tasks',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetTaskParams) => ({
|
||||
|
||||
@@ -62,6 +62,7 @@ export const confluenceGetUserTool: ToolConfig<ConfluenceGetUserParams, Confluen
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/user',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetUserParams) => ({
|
||||
|
||||
@@ -80,6 +80,7 @@ export const confluenceListAttachmentsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceListAttachmentsParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -95,6 +95,7 @@ export const confluenceListBlogPostsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceListBlogPostsParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -108,6 +108,7 @@ export const confluenceListBlogPostsInSpaceTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space-blogposts',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListBlogPostsInSpaceParams) => ({
|
||||
|
||||
@@ -87,6 +87,7 @@ export const confluenceListCommentsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceListCommentsParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -78,6 +78,7 @@ export const confluenceListLabelsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceListLabelsParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -84,6 +84,7 @@ export const confluenceListPagePropertiesTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceListPagePropertiesParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -81,6 +81,7 @@ export const confluenceListPageVersionsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/page-versions',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListPageVersionsParams) => ({
|
||||
|
||||
@@ -111,6 +111,7 @@ export const confluenceListPagesInSpaceTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space-pages',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListPagesInSpaceParams) => ({
|
||||
|
||||
@@ -79,6 +79,7 @@ export const confluenceListSpaceLabelsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceListSpaceLabelsParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -83,6 +83,7 @@ export const confluenceListSpacePermissionsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space-permissions',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListSpacePermissionsParams) => ({
|
||||
|
||||
@@ -79,6 +79,7 @@ export const confluenceListSpacePropertiesTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space-properties',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListSpacePropertiesParams) => ({
|
||||
|
||||
@@ -73,6 +73,7 @@ export const confluenceListSpacesTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceListSpacesParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
|
||||
@@ -111,6 +111,7 @@ export const confluenceListTasksTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/tasks',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListTasksParams) => ({
|
||||
|
||||
@@ -50,6 +50,7 @@ export const confluenceRetrieveTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceRetrieveParams) => {
|
||||
return '/api/tools/confluence/page'
|
||||
},
|
||||
|
||||
@@ -69,6 +69,7 @@ export const confluenceSearchTool: ToolConfig<ConfluenceSearchParams, Confluence
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/search',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceSearchParams) => {
|
||||
|
||||
@@ -91,6 +91,7 @@ export const confluenceSearchInSpaceTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/search-in-space',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceSearchInSpaceParams) => ({
|
||||
|
||||
@@ -54,6 +54,7 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: ConfluenceUpdateParams) => {
|
||||
return '/api/tools/confluence/page'
|
||||
},
|
||||
|
||||
@@ -78,6 +78,7 @@ export const confluenceUpdateBlogPostTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/blogposts',
|
||||
method: 'PUT',
|
||||
headers: (params: ConfluenceUpdateBlogPostParams) => ({
|
||||
|
||||
@@ -66,6 +66,7 @@ export const confluenceUpdateCommentTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/comment',
|
||||
method: 'PUT',
|
||||
headers: (params: ConfluenceUpdateCommentParams) => {
|
||||
|
||||
@@ -79,6 +79,7 @@ export const confluenceUpdateSpaceTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/space',
|
||||
method: 'PUT',
|
||||
headers: (params: ConfluenceUpdateSpaceParams) => ({
|
||||
|
||||
@@ -79,6 +79,7 @@ export const confluenceUpdateTaskTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/tasks',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceUpdateTaskParams) => ({
|
||||
|
||||
@@ -84,6 +84,7 @@ export const confluenceUploadAttachmentTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/confluence/upload-attachment',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceUploadAttachmentParams) => {
|
||||
|
||||
@@ -30,6 +30,7 @@ export const deploymentsGetVersionTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export const deploymentsListVersionsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -10,6 +10,10 @@ import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleDriveUploadTool')
|
||||
|
||||
function usesInternalUploadRoute(params: GoogleDriveToolParams): boolean {
|
||||
return Boolean(params.file)
|
||||
}
|
||||
|
||||
export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResponse> = {
|
||||
id: 'google_drive_upload',
|
||||
name: 'Upload to Google Drive',
|
||||
@@ -67,9 +71,10 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: usesInternalUploadRoute,
|
||||
url: (params) => {
|
||||
// Use custom API route if file is provided, otherwise use Google Drive API directly
|
||||
if (params.file) {
|
||||
if (usesInternalUploadRoute(params)) {
|
||||
return '/api/tools/google_drive/upload'
|
||||
}
|
||||
return 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true'
|
||||
|
||||
@@ -131,6 +131,7 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/grafana/update_alert_rule',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -85,6 +85,7 @@ export const updateDashboardTool: ToolConfig<GrafanaUpdateDashboardParams, ToolR
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/grafana/update_dashboard',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -41,6 +41,7 @@ export const updateFolderTool: ToolConfig<GrafanaUpdateFolderParams, ToolRespons
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/tools/grafana/update_folder',
|
||||
method: 'POST',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
|
||||
@@ -2439,7 +2439,7 @@ describe('executeTool Function', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Automatic Internal Route Detection', () => {
|
||||
describe('Internal Route Trust', () => {
|
||||
let cleanupEnvVars: () => void
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2512,6 +2512,21 @@ describe('Automatic Internal Route Detection', () => {
|
||||
Object.assign(tools, originalTools)
|
||||
})
|
||||
|
||||
it('rejects a caller-controlled relative URL without minting internal credentials', async () => {
|
||||
global.fetch = Object.assign(vi.fn(), { preconnect: vi.fn() }) as typeof fetch
|
||||
|
||||
const result = await executeTool('http_request', {
|
||||
url: '/api/auth/oauth/token',
|
||||
method: 'GET',
|
||||
_context: { userId: 'workflow-owner' },
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toContain('External tool requests require an absolute HTTP(S) URL')
|
||||
expect(mockGenerateInternalToken).not.toHaveBeenCalled()
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('transports only active provenance selected for an internal model input', async () => {
|
||||
const registry = new ResolvedSecretTraceRegistry([
|
||||
{
|
||||
@@ -3533,6 +3548,7 @@ describe('Automatic Internal Route Detection', () => {
|
||||
resourceId: { type: 'string', required: true },
|
||||
},
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params: any) => `/api/resources/${params.resourceId}`,
|
||||
method: 'GET',
|
||||
headers: () => ({ 'Content-Type': 'application/json' }),
|
||||
@@ -5025,6 +5041,14 @@ describe('MCP Tool Execution', () => {
|
||||
})
|
||||
|
||||
describe('Tool request retries', () => {
|
||||
beforeAll(() => {
|
||||
;(tools.http_request.request as { internal?: true }).internal = true
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
;(tools.http_request.request as { internal?: true }).internal = undefined
|
||||
})
|
||||
|
||||
function makeJsonResponse(
|
||||
status: number,
|
||||
body: unknown,
|
||||
|
||||
@@ -48,8 +48,9 @@ export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumen
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents`,
|
||||
url: (params) => `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents`,
|
||||
method: 'POST',
|
||||
secretProvenance: {
|
||||
request: selectKnowledgeDocumentWriteSecretProvenance,
|
||||
|
||||
@@ -29,9 +29,10 @@ export const knowledgeDeleteChunkTool: ToolConfig<any, KnowledgeDeleteChunkRespo
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks/${params.chunkId}`,
|
||||
`/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}/chunks/${encodeURIComponent(params.chunkId)}`,
|
||||
method: 'DELETE',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -23,8 +23,10 @@ export const knowledgeDeleteDocumentTool: ToolConfig<any, KnowledgeDeleteDocumen
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}`,
|
||||
url: (params) =>
|
||||
`/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}`,
|
||||
method: 'DELETE',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -24,8 +24,10 @@ export const knowledgeGetConnectorTool: ToolConfig<any, KnowledgeGetConnectorRes
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/connectors/${params.connectorId}`,
|
||||
url: (params) =>
|
||||
`/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/connectors/${encodeURIComponent(params.connectorId)}`,
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -24,8 +24,10 @@ export const knowledgeGetDocumentTool: ToolConfig<any, KnowledgeGetDocumentRespo
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}`,
|
||||
url: (params) =>
|
||||
`/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}`,
|
||||
method: 'GET',
|
||||
secretProvenance: { response: { incomplete: 'reject' } },
|
||||
headers: () => ({
|
||||
|
||||
@@ -48,6 +48,7 @@ export const knowledgeListChunksTool: ToolConfig<any, KnowledgeListChunksRespons
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
@@ -57,7 +58,7 @@ export const knowledgeListChunksTool: ToolConfig<any, KnowledgeListChunksRespons
|
||||
queryParams.set('limit', String(Math.max(1, Math.min(100, Number(params.limit)))))
|
||||
if (params.offset != null) queryParams.set('offset', String(params.offset))
|
||||
const qs = queryParams.toString()
|
||||
return `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks${qs ? `?${qs}` : ''}`
|
||||
return `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}/chunks${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
secretProvenance: { response: { incomplete: 'reject' } },
|
||||
|
||||
@@ -18,8 +18,9 @@ export const knowledgeListConnectorsTool: ToolConfig<any, KnowledgeListConnector
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/connectors`,
|
||||
url: (params) => `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/connectors`,
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -41,6 +41,7 @@ export const knowledgeListDocumentsTool: ToolConfig<any, KnowledgeListDocumentsR
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => {
|
||||
const queryParams = new URLSearchParams()
|
||||
@@ -49,7 +50,7 @@ export const knowledgeListDocumentsTool: ToolConfig<any, KnowledgeListDocumentsR
|
||||
if (params.limit != null) queryParams.set('limit', String(params.limit))
|
||||
if (params.offset != null) queryParams.set('offset', String(params.offset))
|
||||
const qs = queryParams.toString()
|
||||
return `/api/knowledge/${params.knowledgeBaseId}/documents${qs ? `?${qs}` : ''}`
|
||||
return `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
secretProvenance: { response: { incomplete: 'reject' } },
|
||||
|
||||
@@ -17,8 +17,9 @@ export const knowledgeListTagsTool: ToolConfig<any, KnowledgeListTagsResponse> =
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/tag-definitions`,
|
||||
url: (params) => `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/tag-definitions`,
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -85,6 +85,7 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: () => '/api/knowledge/search',
|
||||
method: 'POST',
|
||||
|
||||
@@ -23,9 +23,10 @@ export const knowledgeTriggerSyncTool: ToolConfig<any, KnowledgeTriggerSyncRespo
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/connectors/${params.connectorId}/sync`,
|
||||
`/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/connectors/${encodeURIComponent(params.connectorId)}/sync`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -41,9 +41,10 @@ export const knowledgeUpdateChunkTool: ToolConfig<any, KnowledgeUpdateChunkRespo
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks/${params.chunkId}`,
|
||||
`/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}/chunks/${encodeURIComponent(params.chunkId)}`,
|
||||
method: 'PUT',
|
||||
secretProvenance: {
|
||||
request: (params) =>
|
||||
|
||||
@@ -29,9 +29,10 @@ export const knowledgeUploadChunkTool: ToolConfig<any, KnowledgeUploadChunkRespo
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) =>
|
||||
`/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks`,
|
||||
`/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}/chunks`,
|
||||
method: 'POST',
|
||||
secretProvenance: {
|
||||
request: () => [{ key: 'chunk-content', inputPaths: [['content']] }],
|
||||
|
||||
@@ -60,8 +60,10 @@ export const knowledgeUpsertDocumentTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
internalAuth: 'executor_delegation',
|
||||
url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents/upsert`,
|
||||
url: (params) =>
|
||||
`/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/upsert`,
|
||||
method: 'POST',
|
||||
secretProvenance: {
|
||||
request: selectKnowledgeDocumentWriteSecretProvenance,
|
||||
|
||||
@@ -128,6 +128,7 @@ export const llmChatTool: ToolConfig<LLMChatParams, LLMChatResponse> = {
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: () => '/api/providers',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
|
||||
@@ -18,6 +18,7 @@ export const logsGetExecutionTool: ToolConfig<LogsGetExecutionParams, LogsGetExe
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => `/api/logs/execution/${encodeURIComponent(params.executionId)}`,
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
|
||||
@@ -17,6 +17,7 @@ export const logsGetTool: ToolConfig<LogsGetParams, LogsGetResponse> = {
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -21,6 +21,7 @@ export const logsGetRunDetailsTool: ToolConfig<LogsGetRunDetailsParams, LogsGetR
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -78,6 +78,7 @@ export const logsQueryTool: ToolConfig<LogsQueryParams, LogsQueryResponse> = {
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -99,6 +99,7 @@ export const logsQueryRunsTool: ToolConfig<LogsQueryRunsParams, LogsQueryRunsRes
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -25,6 +25,7 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -25,6 +25,7 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const memoryGetAllTool: ToolConfig<any, MemoryResponse> = {
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
|
||||
@@ -47,6 +47,7 @@ export const deleteChatMessageTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
url: (params) => {
|
||||
const chatId = params.chatId?.trim()
|
||||
const messageId = params.messageId?.trim()
|
||||
|
||||
@@ -4,6 +4,10 @@ import type {
|
||||
} from '@/tools/microsoft_teams/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function usesInternalChannelRoute(params: MicrosoftTeamsToolParams): boolean {
|
||||
return Boolean(params.files?.length || /<at>[^<]+<\/at>/i.test(params.content || ''))
|
||||
}
|
||||
|
||||
export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsWriteResponse> = {
|
||||
id: 'microsoft_teams_write_channel',
|
||||
name: 'Write to Microsoft Teams Channel',
|
||||
@@ -62,6 +66,7 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: usesInternalChannelRoute,
|
||||
url: (params) => {
|
||||
const teamId = params.teamId?.trim()
|
||||
if (!teamId) {
|
||||
@@ -79,8 +84,7 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
|
||||
}
|
||||
|
||||
// If content contains mentions, use custom API route for mention resolution
|
||||
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
|
||||
if (hasMentions) {
|
||||
if (usesInternalChannelRoute(params)) {
|
||||
return '/api/tools/microsoft_teams/write_channel'
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import type {
|
||||
} from '@/tools/microsoft_teams/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function usesInternalChatRoute(params: MicrosoftTeamsToolParams): boolean {
|
||||
return Boolean(params.files?.length || /<at>[^<]+<\/at>/i.test(params.content || ''))
|
||||
}
|
||||
|
||||
export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsWriteResponse> = {
|
||||
id: 'microsoft_teams_write_chat',
|
||||
name: 'Write to Microsoft Teams Chat',
|
||||
@@ -54,6 +58,7 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: usesInternalChatRoute,
|
||||
url: (params) => {
|
||||
// Ensure chatId is valid
|
||||
const chatId = params.chatId?.trim()
|
||||
@@ -67,8 +72,7 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
|
||||
}
|
||||
|
||||
// If content contains mentions, use custom API route for mention resolution
|
||||
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
|
||||
if (hasMentions) {
|
||||
if (usesInternalChatRoute(params)) {
|
||||
return '/api/tools/microsoft_teams/write_chat'
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ import type { OneDriveToolParams, OneDriveUploadResponse } from '@/tools/onedriv
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('OneDriveUploadTool')
|
||||
const EXCEL_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
|
||||
function usesInternalUploadRoute(params: OneDriveToolParams): boolean {
|
||||
return Boolean(params.file || params.mimeType === EXCEL_MIME_TYPE)
|
||||
}
|
||||
|
||||
export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse> = {
|
||||
id: 'onedrive_upload',
|
||||
@@ -56,10 +61,9 @@ export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse>
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: usesInternalUploadRoute,
|
||||
url: (params) => {
|
||||
const isExcelFile =
|
||||
params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
if (params.file || isExcelFile) {
|
||||
if (usesInternalUploadRoute(params)) {
|
||||
return '/api/tools/onedrive/upload'
|
||||
}
|
||||
|
||||
@@ -75,17 +79,11 @@ export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse>
|
||||
}
|
||||
return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content`
|
||||
},
|
||||
method: (params) => {
|
||||
const isExcelFile =
|
||||
params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
return params.file || isExcelFile ? 'POST' : 'PUT'
|
||||
},
|
||||
method: (params) => (usesInternalUploadRoute(params) ? 'POST' : 'PUT'),
|
||||
headers: (params) => {
|
||||
const headers: Record<string, string> = {}
|
||||
const isExcelFile =
|
||||
params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
|
||||
if (params.file || isExcelFile) {
|
||||
if (usesInternalUploadRoute(params)) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
} else {
|
||||
headers.Authorization = `Bearer ${params.accessToken}`
|
||||
@@ -94,10 +92,7 @@ export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse>
|
||||
return headers
|
||||
},
|
||||
body: (params) => {
|
||||
const isExcelFile =
|
||||
params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
|
||||
if (params.file || isExcelFile) {
|
||||
if (usesInternalUploadRoute(params)) {
|
||||
return {
|
||||
accessToken: params.accessToken,
|
||||
fileName: params.fileName,
|
||||
@@ -115,10 +110,7 @@ export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse>
|
||||
transformResponse: async (response: Response, params?: OneDriveToolParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
const isExcelFile =
|
||||
params?.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
|
||||
if ((params?.file || isExcelFile) && data.success !== undefined) {
|
||||
if (params && usesInternalUploadRoute(params) && data.success !== undefined) {
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to upload file')
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PRIVATE_MODEL_INPUT_PROVENANCE_HEADER } from '@/lib/execution/model-input-provenance'
|
||||
import {
|
||||
RESOLVED_SECRET_PROVENANCE_FIELD,
|
||||
RESOLVED_SECRET_PROVENANCE_METADATA_V1,
|
||||
} from '@/lib/execution/private-tool-metadata'
|
||||
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
|
||||
import { requestTool } from '@/tools/http/request'
|
||||
import { webhookRequestTool } from '@/tools/http/webhook_request'
|
||||
import { tools } from '@/tools/registry'
|
||||
import { prepareToolRequest } from '@/tools/request-transport'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
@@ -14,6 +16,263 @@ vi.unmock('@/tools/registry')
|
||||
const privateProvenanceTools = Object.entries(tools).filter(
|
||||
([, tool]) => tool.request.modelInput?.mode === 'private-provenance'
|
||||
)
|
||||
const dynamicRouteTools = Object.entries(tools).filter(
|
||||
([, tool]) => typeof tool.request.url === 'function' && !tool.directExecution
|
||||
)
|
||||
const PROBE_CONTEXT = {
|
||||
workflowId: 'workflow-probe',
|
||||
workspaceId: 'workspace-probe',
|
||||
userId: 'user-probe',
|
||||
executionId: 'execution-probe',
|
||||
} as const
|
||||
const PROBE_FILE = {
|
||||
name: 'probe.txt',
|
||||
mimeType: 'text/plain',
|
||||
data: 'data:text/plain;base64,cHJvYmU=',
|
||||
} as const
|
||||
const EXCEL_MIME_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
|
||||
function createSchemaProbeParams(
|
||||
tool: ToolConfig,
|
||||
includeOptional: boolean,
|
||||
adversarialPathStrings = false
|
||||
) {
|
||||
const params: Record<string, unknown> = {
|
||||
_context: PROBE_CONTEXT,
|
||||
...PROBE_CONTEXT,
|
||||
}
|
||||
|
||||
for (const [name, schema] of Object.entries(tool.params)) {
|
||||
if (!includeOptional && !schema.required) continue
|
||||
|
||||
let value: unknown
|
||||
if (name === 'mimeType') value = includeOptional ? EXCEL_MIME_TYPE : undefined
|
||||
else if (name === 'content') value = includeOptional ? '<at>Probe User</at>' : 'Plain text'
|
||||
else if (/(?:url|host)$/i.test(name)) {
|
||||
value = 'https://example.com'
|
||||
} else if (name === 'method') value = 'GET'
|
||||
else if (schema.type === 'file')
|
||||
value = includeOptional || schema.required ? PROBE_FILE : undefined
|
||||
else if (schema.type === 'file[]') value = includeOptional ? [PROBE_FILE] : []
|
||||
else if (schema.type === 'array') value = includeOptional ? [{ id: 'item-probe' }] : []
|
||||
else if (schema.type === 'object') value = {}
|
||||
else if (schema.type === 'json') value = includeOptional ? [{ id: 'item-probe' }] : {}
|
||||
else if (schema.type === 'number') value = 1
|
||||
else if (schema.type === 'boolean') value = includeOptional
|
||||
else if (adversarialPathStrings) value = '../probe?next=/api'
|
||||
else value = name.toLowerCase().includes('id') ? 'id-probe' : 'probe'
|
||||
|
||||
if (value !== undefined) params[name] = value
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
function isAbsoluteHttpUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function createRequestTool(
|
||||
url: string | ((params: Record<string, unknown>) => string),
|
||||
internal?: true | ((params: Record<string, unknown>) => boolean)
|
||||
): ToolConfig {
|
||||
return {
|
||||
id: 'request_transport_probe',
|
||||
name: 'Request transport probe',
|
||||
description: 'Tests request transport trust decisions',
|
||||
version: '1.0.0',
|
||||
params: {},
|
||||
request: {
|
||||
url,
|
||||
method: 'GET',
|
||||
headers: () => ({}),
|
||||
...(internal ? { internal } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('request URL trust', () => {
|
||||
it('keeps literal Sim API routes internal', () => {
|
||||
const request = prepareToolRequest(createRequestTool('/api/tools/probe'), {})
|
||||
|
||||
expect(request.isInternalRoute).toBe(true)
|
||||
})
|
||||
|
||||
it('requires dynamic Sim API routes to opt in', () => {
|
||||
const untrustedTool = createRequestTool(() => '/api/tools/probe')
|
||||
const internalTool = createRequestTool(() => '/api/tools/probe', true)
|
||||
|
||||
expect(() => prepareToolRequest(untrustedTool, {})).toThrow(
|
||||
'External tool requests require an absolute HTTP(S) URL'
|
||||
)
|
||||
expect(prepareToolRequest(internalTool, {}).isInternalRoute).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects an internal definition that resolves outside the Sim API', () => {
|
||||
const tool = createRequestTool(() => 'https://example.com', true)
|
||||
|
||||
expect(() => prepareToolRequest(tool, {})).toThrow(
|
||||
'Internal tool requests must target a Sim API route'
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'/api/tools/probe/../auth/oauth/token',
|
||||
'/api/tools/probe/%2e%2e/auth/oauth/token',
|
||||
'/api/tools/probe\\..\\auth/oauth/token',
|
||||
'/api/tools/probe value',
|
||||
'/api/tools/probe?next=/auth/oauth/token',
|
||||
])('rejects a non-canonical internal route: %s', (url) => {
|
||||
expect(() =>
|
||||
prepareToolRequest(
|
||||
createRequestTool(() => url, true),
|
||||
{}
|
||||
)
|
||||
).toThrow('Internal tool requests must target a Sim API route')
|
||||
})
|
||||
|
||||
it('allows encoded path segments and query values on internal routes', () => {
|
||||
const request = prepareToolRequest(
|
||||
createRequestTool(() => '/api/tools/probe/probe%20value?next=..%2Fsafe', true),
|
||||
{}
|
||||
)
|
||||
|
||||
expect(request.isInternalRoute).toBe(true)
|
||||
})
|
||||
|
||||
it('supports definition-owned trust for a conditional URL builder', () => {
|
||||
const tool = createRequestTool(
|
||||
(params) => (params.useInternal === true ? '/api/tools/probe' : 'https://example.com/probe'),
|
||||
(params) => params.useInternal === true
|
||||
)
|
||||
|
||||
expect(prepareToolRequest(tool, { useInternal: true }).isInternalRoute).toBe(true)
|
||||
expect(prepareToolRequest(tool, { useInternal: false }).isInternalRoute).toBe(false)
|
||||
})
|
||||
|
||||
it.each(['relative/path', 'ftp://example.com/file', 'javascript:alert(1)'])(
|
||||
'rejects an invalid external URL: %s',
|
||||
(url) => {
|
||||
const tool = createRequestTool(() => url)
|
||||
|
||||
expect(() => prepareToolRequest(tool, {})).toThrow(
|
||||
'External tool requests require an absolute HTTP(S) URL'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('allows absolute HTTP and HTTPS URLs on the external transport', () => {
|
||||
expect(
|
||||
prepareToolRequest(
|
||||
createRequestTool(() => 'http://example.com'),
|
||||
{}
|
||||
).isInternalRoute
|
||||
).toBe(false)
|
||||
expect(
|
||||
prepareToolRequest(
|
||||
createRequestTool(() => 'https://example.com'),
|
||||
{}
|
||||
).isInternalRoute
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['http_request', requestTool, { url: '/api/auth/oauth/token', method: 'GET' }],
|
||||
['webhook_request', webhookRequestTool, { url: '/api/auth/oauth/token', body: {} }],
|
||||
])('rejects a relative URL from %s', (_toolId, tool, params) => {
|
||||
expect(() => prepareToolRequest(tool, params)).toThrow(
|
||||
'External tool requests require an absolute HTTP(S) URL'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dynamic internal route registry invariant', () => {
|
||||
it('covers every dynamic registry URL, with explicit declarations as the trust policy', () => {
|
||||
expect(dynamicRouteTools.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('probes every dynamic registry URL against its declared trust policy', () => {
|
||||
let exercisedTools = 0
|
||||
|
||||
for (const [toolId, tool] of dynamicRouteTools) {
|
||||
const urlBuilder = tool.request.url
|
||||
if (typeof urlBuilder !== 'function') throw new Error(`${toolId} must have a dynamic URL`)
|
||||
const policy = tool.request.internal
|
||||
const observations: Array<{ internal: boolean; url: string }> = []
|
||||
const scenarios = [
|
||||
createSchemaProbeParams(tool, false),
|
||||
createSchemaProbeParams(tool, true),
|
||||
createSchemaProbeParams(tool, true, true),
|
||||
]
|
||||
|
||||
for (const params of scenarios) {
|
||||
let url: string
|
||||
let internal: boolean
|
||||
try {
|
||||
url = urlBuilder(params as never)
|
||||
internal = typeof policy === 'function' ? policy(params as never) : policy === true
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!url && policy === undefined) continue
|
||||
observations.push({ internal, url })
|
||||
expect(
|
||||
internal ? url.startsWith('/api/') : isAbsoluteHttpUrl(url),
|
||||
`${toolId} resolved ${url} outside its declared request trust`
|
||||
).toBe(true)
|
||||
expect(() =>
|
||||
prepareToolRequest(
|
||||
createRequestTool(() => url, internal ? true : undefined),
|
||||
{}
|
||||
)
|
||||
).not.toThrow()
|
||||
}
|
||||
|
||||
if (observations.length === 0) {
|
||||
expect(
|
||||
policy,
|
||||
`${toolId} declares internal trust but could not be exercised by the route probe`
|
||||
).toBeUndefined()
|
||||
continue
|
||||
}
|
||||
exercisedTools += 1
|
||||
if (policy !== undefined) {
|
||||
expect(
|
||||
observations.some((observation) => observation.internal),
|
||||
`${toolId} did not exercise its internal route`
|
||||
).toBe(true)
|
||||
}
|
||||
if (typeof policy === 'function') {
|
||||
expect(
|
||||
observations.some((observation) => !observation.internal),
|
||||
`${toolId} did not exercise its external route`
|
||||
).toBe(true)
|
||||
}
|
||||
}
|
||||
|
||||
expect(exercisedTools).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('validates every literal internal registry route', () => {
|
||||
const literalInternalRoutes = Object.entries(tools).filter(
|
||||
([, tool]) => typeof tool.request.url === 'string' && tool.request.url.startsWith('/api/')
|
||||
)
|
||||
|
||||
expect(literalInternalRoutes.length).toBeGreaterThan(0)
|
||||
for (const [toolId, tool] of literalInternalRoutes) {
|
||||
expect(
|
||||
() => prepareToolRequest(createRequestTool(tool.request.url as string), {}),
|
||||
`${toolId} has a non-canonical literal internal route`
|
||||
).not.toThrow()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('private-provenance tool registry invariant', () => {
|
||||
it('covers at least one registered tool', () => {
|
||||
|
||||
@@ -17,6 +17,9 @@ const PRIVATE_MODEL_INPUT_EXTERNAL_URL_ERROR_MESSAGE =
|
||||
'Private model input provenance is only supported for internal routes'
|
||||
const PRIVATE_SECRET_PROVENANCE_EXTERNAL_URL_ERROR_MESSAGE =
|
||||
'Private secret provenance is only supported for internal routes'
|
||||
const EXTERNAL_REQUEST_URL_ERROR_MESSAGE = 'External tool requests require an absolute HTTP(S) URL'
|
||||
const INTERNAL_REQUEST_URL_ERROR_MESSAGE = 'Internal tool requests must target a Sim API route'
|
||||
const INTERNAL_REQUEST_BASE_URL = 'http://sim.internal'
|
||||
|
||||
export interface PreparedToolRequest {
|
||||
url: string
|
||||
@@ -156,9 +159,11 @@ function collectProvenanceSensitiveHeaders(
|
||||
function formatToolRequest(
|
||||
tool: ToolConfig,
|
||||
params: Record<string, any>,
|
||||
registry?: ResolvedSecretTraceRegistry
|
||||
registry: ResolvedSecretTraceRegistry | undefined,
|
||||
isInternalRoute: boolean
|
||||
): PreparedToolRequest {
|
||||
const url = typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
|
||||
assertRequestUrlMatchesTrust(url, isInternalRoute)
|
||||
const method =
|
||||
typeof tool.request.method === 'function'
|
||||
? tool.request.method(params)
|
||||
@@ -217,7 +222,50 @@ function formatToolRequest(
|
||||
proxyUrl,
|
||||
stripAuthOnRedirect: tool.request.stripAuthOnRedirect,
|
||||
redirectPolicy,
|
||||
isInternalRoute: url.startsWith('/api/'),
|
||||
isInternalRoute,
|
||||
}
|
||||
}
|
||||
|
||||
function isInternalRequestDefinition(tool: ToolConfig, params: Record<string, any>): boolean {
|
||||
if (typeof tool.request.internal === 'function') return tool.request.internal(params)
|
||||
return (
|
||||
tool.request.internal === true ||
|
||||
(typeof tool.request.url === 'string' && tool.request.url.startsWith('/api/'))
|
||||
)
|
||||
}
|
||||
|
||||
function assertRequestUrlMatchesTrust(url: string, isInternalRoute: boolean): void {
|
||||
if (isInternalRoute) {
|
||||
const pathEnd = url.search(/[?#]/)
|
||||
const rawPathname = pathEnd === -1 ? url : url.slice(0, pathEnd)
|
||||
let parsedUrl: URL
|
||||
try {
|
||||
parsedUrl = new URL(url, INTERNAL_REQUEST_BASE_URL)
|
||||
} catch {
|
||||
throw new Error(INTERNAL_REQUEST_URL_ERROR_MESSAGE)
|
||||
}
|
||||
const rawSearch = parsedUrl.search.startsWith('?') ? parsedUrl.search.slice(1) : ''
|
||||
const canonicalSearch = new URLSearchParams(parsedUrl.search).toString()
|
||||
if (
|
||||
!url.startsWith('/api/') ||
|
||||
parsedUrl.origin !== INTERNAL_REQUEST_BASE_URL ||
|
||||
parsedUrl.pathname !== rawPathname ||
|
||||
rawSearch !== canonicalSearch ||
|
||||
parsedUrl.hash
|
||||
) {
|
||||
throw new Error(INTERNAL_REQUEST_URL_ERROR_MESSAGE)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let parsedUrl: URL
|
||||
try {
|
||||
parsedUrl = new URL(url)
|
||||
} catch {
|
||||
throw new Error(EXTERNAL_REQUEST_URL_ERROR_MESSAGE)
|
||||
}
|
||||
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||
throw new Error(EXTERNAL_REQUEST_URL_ERROR_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,23 +275,23 @@ export function prepareToolRequest(
|
||||
params: Record<string, any>,
|
||||
registry?: ResolvedSecretTraceRegistry
|
||||
): PreparedToolRequest {
|
||||
const configuredUrl =
|
||||
typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
|
||||
const modelInput = tool.request.modelInput
|
||||
const secretProvenance = tool.request.secretProvenance
|
||||
const configuredAsInternal = isInternalRequestDefinition(tool, params)
|
||||
const hasPrivateModelInputProvenance =
|
||||
modelInput?.mode === 'private-provenance' ||
|
||||
(modelInput?.mode === 'project' && modelInput.privateInputPaths !== undefined)
|
||||
|
||||
if (hasPrivateModelInputProvenance && !configuredUrl.startsWith('/api/')) {
|
||||
if (hasPrivateModelInputProvenance && !configuredAsInternal) {
|
||||
throw new Error(PRIVATE_MODEL_INPUT_EXTERNAL_URL_ERROR_MESSAGE)
|
||||
}
|
||||
if (secretProvenance && !configuredUrl.startsWith('/api/')) {
|
||||
if (secretProvenance && !configuredAsInternal) {
|
||||
throw new Error(PRIVATE_SECRET_PROVENANCE_EXTERNAL_URL_ERROR_MESSAGE)
|
||||
}
|
||||
|
||||
const requestInput = projectToolModelInputParams(tool, params, registry)
|
||||
const request = formatToolRequest(tool, requestInput, registry)
|
||||
const isInternalRoute = isInternalRequestDefinition(tool, requestInput)
|
||||
const request = formatToolRequest(tool, requestInput, registry, isInternalRoute)
|
||||
if (hasPrivateModelInputProvenance && !request.isInternalRoute) {
|
||||
throw new Error(PRIVATE_MODEL_INPUT_EXTERNAL_URL_ERROR_MESSAGE)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
modelInput: {
|
||||
mode: 'project',
|
||||
select: (params) => ({ query: params.query }),
|
||||
|
||||
@@ -35,11 +35,13 @@ export const tableBatchInsertRowsTool: ToolConfig<
|
||||
},
|
||||
|
||||
request: {
|
||||
internal: true,
|
||||
secretProvenance: {
|
||||
request: (params) => selectTableRowSecretProvenance(params.rows, 'rows'),
|
||||
response: { incomplete: 'propagate' },
|
||||
},
|
||||
url: (params: TableBatchInsertParams) => `/api/table/${params.tableId}/rows`,
|
||||
url: (params: TableBatchInsertParams) =>
|
||||
`/api/table/${encodeURIComponent(params.tableId)}/rows`,
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user