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:
Vikhyath Mondreti
2026-08-25 19:35:27 -07:00
committed by GitHub
parent 2d85c0d1e1
commit 77b5ae3b29
118 changed files with 1268 additions and 60 deletions
+1
View File
@@ -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' }),
+1
View File
@@ -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' }),
+1
View File
@@ -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' }),
+1
View File
@@ -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' }),
+1
View File
@@ -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' }),
+1
View File
@@ -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' }),
+1
View File
@@ -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' }),
+1
View File
@@ -44,6 +44,7 @@ export const agiloftSavedSearchTool: ToolConfig<
},
request: {
internal: true,
url: () => '/api/tools/agiloft/saved_search',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
+1
View File
@@ -77,6 +77,7 @@ export const agiloftSearchRecordsTool: ToolConfig<
},
request: {
internal: true,
url: () => '/api/tools/agiloft/search_records',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
+1
View File
@@ -51,6 +51,7 @@ export const agiloftSelectRecordsTool: ToolConfig<
},
request: {
internal: true,
url: () => '/api/tools/agiloft/select_records',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
+1
View File
@@ -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' }),
+1
View File
@@ -66,6 +66,7 @@ export const agiloftUpsertRecordTool: ToolConfig<
},
request: {
internal: true,
url: () => '/api/tools/agiloft/upsert_record',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
+1
View File
@@ -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) => {
+1
View File
@@ -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) => ({
+1
View File
@@ -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,
+1
View File
@@ -71,6 +71,7 @@ export const confluenceGetSpaceTool: ToolConfig<
},
request: {
internal: true,
url: (params: ConfluenceGetSpaceParams) => {
const query = new URLSearchParams({
domain: params.domain,
+1
View File
@@ -70,6 +70,7 @@ export const confluenceGetTaskTool: ToolConfig<ConfluenceGetTaskParams, Confluen
},
request: {
internal: true,
url: () => '/api/tools/confluence/tasks',
method: 'POST',
headers: (params: ConfluenceGetTaskParams) => ({
+1
View File
@@ -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,
+1
View File
@@ -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) => ({
+1
View File
@@ -73,6 +73,7 @@ export const confluenceListSpacesTool: ToolConfig<
},
request: {
internal: true,
url: (params: ConfluenceListSpacesParams) => {
const query = new URLSearchParams({
domain: params.domain,
+1
View File
@@ -111,6 +111,7 @@ export const confluenceListTasksTool: ToolConfig<
},
request: {
internal: true,
url: () => '/api/tools/confluence/tasks',
method: 'POST',
headers: (params: ConfluenceListTasksParams) => ({
+1
View File
@@ -50,6 +50,7 @@ export const confluenceRetrieveTool: ToolConfig<
},
request: {
internal: true,
url: (params: ConfluenceRetrieveParams) => {
return '/api/tools/confluence/page'
},
+1
View File
@@ -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) => ({
+1
View File
@@ -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) => ({
+1
View File
@@ -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) {
+6 -1
View File
@@ -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' }),
+1
View File
@@ -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' }),
+25 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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',
+3 -1
View File
@@ -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',
+3 -1
View File
@@ -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',
+3 -1
View File
@@ -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: () => ({
+2 -1
View File
@@ -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' } },
+2 -1
View File
@@ -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',
+2 -1
View File
@@ -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' } },
+2 -1
View File
@@ -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',
+1
View File
@@ -85,6 +85,7 @@ export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {
},
request: {
internal: true,
internalAuth: 'executor_delegation',
url: () => '/api/knowledge/search',
method: 'POST',
+2 -1
View File
@@ -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',
+2 -1
View File
@@ -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) =>
+2 -1
View File
@@ -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']] }],
+3 -1
View File
@@ -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,
+1
View File
@@ -128,6 +128,7 @@ export const llmChatTool: ToolConfig<LLMChatParams, LLMChatResponse> = {
},
request: {
internal: true,
url: () => '/api/providers',
method: 'POST',
headers: () => ({
+1
View File
@@ -18,6 +18,7 @@ export const logsGetExecutionTool: ToolConfig<LogsGetExecutionParams, LogsGetExe
},
request: {
internal: true,
url: (params) => `/api/logs/execution/${encodeURIComponent(params.executionId)}`,
method: 'GET',
headers: () => ({
+1
View File
@@ -17,6 +17,7 @@ export const logsGetTool: ToolConfig<LogsGetParams, LogsGetResponse> = {
},
request: {
internal: true,
url: (params) => {
const workspaceId = params._context?.workspaceId
if (!workspaceId) {
+1
View File
@@ -21,6 +21,7 @@ export const logsGetRunDetailsTool: ToolConfig<LogsGetRunDetailsParams, LogsGetR
},
request: {
internal: true,
url: (params) => {
const workspaceId = params._context?.workspaceId
if (!workspaceId) {
+1
View File
@@ -78,6 +78,7 @@ export const logsQueryTool: ToolConfig<LogsQueryParams, LogsQueryResponse> = {
},
request: {
internal: true,
url: (params) => {
const workspaceId = params._context?.workspaceId
if (!workspaceId) {
+1
View File
@@ -99,6 +99,7 @@ export const logsQueryRunsTool: ToolConfig<LogsQueryRunsParams, LogsQueryRunsRes
},
request: {
internal: true,
url: (params) => {
const workspaceId = params._context?.workspaceId
if (!workspaceId) {
+1
View File
@@ -25,6 +25,7 @@ export const memoryDeleteTool: ToolConfig<any, MemoryResponse> = {
},
request: {
internal: true,
url: (params) => {
const workspaceId = params._context?.workspaceId
if (!workspaceId) {
+1
View File
@@ -25,6 +25,7 @@ export const memoryGetTool: ToolConfig<any, MemoryResponse> = {
},
request: {
internal: true,
url: (params) => {
const workspaceId = params._context?.workspaceId
if (!workspaceId) {
+1
View File
@@ -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'
}
+6 -2
View File
@@ -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'
}
+11 -19
View File
@@ -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')
}
+260 -1
View 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', () => {
+55 -7
View File
@@ -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)
}
+1
View File
@@ -18,6 +18,7 @@ export const searchTool: ToolConfig<SearchParams, SearchResponse> = {
},
request: {
internal: true,
modelInput: {
mode: 'project',
select: (params) => ({ query: params.query }),
+3 -1
View File
@@ -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