Skip to content

Commit b830c2e

Browse files
authored
fix(integrations): validate servicenow, salesforce, and hubspot against vendor docs (#10027)
* fix(servicenow): keep model-supplied state, comments, close code/notes, query, limit, fields, and display value on tool calls * fix(salesforce): resolve absolute nextRecordsUrl values on the connected instance and encode path ids * fix(hubspot): guard record id path segments and stop get-op properties leaking into search * fix(servicenow): never send a stored JSON record body as a tool-call field projection * fix(servicenow): type the mapper context as optional so direct callers type-check
1 parent d10b2f2 commit b830c2e

60 files changed

Lines changed: 334 additions & 106 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/sim/blocks/blocks/hubspot.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { HubspotIcon } from '@/components/icons'
22
import { getScopesForService } from '@/lib/oauth/utils'
33
import type { BlockConfig, BlockMeta } from '@/blocks/types'
44
import { AuthMode, IntegrationType } from '@/blocks/types'
5+
import { toolCallArg } from '@/blocks/utils'
56
import { getTrigger } from '@/triggers'
67

78
export const HubSpotBlock: BlockConfig = {
@@ -1631,7 +1632,7 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
16311632
throw new Error(`Unknown operation: ${params.operation}`)
16321633
}
16331634
},
1634-
params: (params) => {
1635+
params: (params, context) => {
16351636
const {
16361637
oauthCredential,
16371638
operation,
@@ -1698,8 +1699,8 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
16981699
'search_line_items',
16991700
'search_quotes',
17001701
]
1701-
if (searchProperties && searchOps.includes(operation as string)) {
1702-
cleanParams.properties = searchProperties
1702+
if (searchOps.includes(operation as string)) {
1703+
cleanParams.properties = searchProperties || toolCallArg(context, properties)
17031704
}
17041705

17051706
if (filterGroups && searchOps.includes(operation as string)) {

‎apps/sim/blocks/blocks/servicenow.test.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,75 @@ describe('ServiceNow state written after the split is never re-migrated', () =>
159159
expect(second.blocks['block-1'].subBlocks.readFields?.value).toBe('number,state')
160160
})
161161
})
162+
163+
/**
164+
* A tool call (agent, Pi, Human v2) passes the model's args under the tool param
165+
* names. Operations whose value has a dedicated, differently-named subblock must
166+
* fall back to the model's arg instead of overwriting it with the blank subblock.
167+
*/
168+
describe('ServiceNow tool-call args reach the tool', () => {
169+
function mapToolCall(operation: string, args: Record<string, unknown>): Record<string, unknown> {
170+
const transform = ServiceNowBlock.tools.config?.params
171+
if (!transform) throw new Error('ServiceNow block has no params transform')
172+
const params = { ...CREDENTIALS, ...args }
173+
return { ...params, ...transform(params, { operation }) }
174+
}
175+
176+
it('keeps the model close code and notes on Resolve Incident', () => {
177+
const mapped = mapToolCall('servicenow_resolve_incident', {
178+
sysId: 'abc',
179+
closeCode: 'Solution provided',
180+
closeNotes: 'Restarted the service',
181+
})
182+
183+
expect(mapped.closeCode).toBe('Solution provided')
184+
expect(mapped.closeNotes).toBe('Restarted the service')
185+
})
186+
187+
it('keeps the model state filter, projection, and display mode on List Incidents', () => {
188+
const mapped = mapToolCall('servicenow_list_incidents', {
189+
state: '2',
190+
fields: 'number,state',
191+
displayValue: 'false',
192+
})
193+
194+
expect(mapped.state).toBe('2')
195+
expect(mapped.fields).toBe('number,state')
196+
expect(mapped.displayValue).toBe('false')
197+
})
198+
199+
it('does not send a stored Create/Update JSON body as the List Incidents projection', () => {
200+
const mapped = mapToolCall('servicenow_list_incidents', {
201+
fields: '{"short_description":"Printer down"}',
202+
})
203+
204+
expect(mapped.fields).toBeUndefined()
205+
})
206+
207+
it('keeps the model comment on Update Approval and the search text on Search Knowledge', () => {
208+
expect(
209+
mapToolCall('servicenow_update_approval', {
210+
approvalSysId: 'abc',
211+
decision: 'approved',
212+
comments: 'Looks good',
213+
}).comments
214+
).toBe('Looks good')
215+
expect(mapToolCall('servicenow_search_knowledge', { query: 'vpn' }).query).toBe('vpn')
216+
})
217+
218+
it('keeps the model limit on List Attachments', () => {
219+
const mapped = mapToolCall('servicenow_list_attachments', {
220+
tableName: 'incident',
221+
recordSysId: 'abc',
222+
limit: 5,
223+
})
224+
225+
expect(mapped.limit).toBe(5)
226+
})
227+
228+
it('prefers a configured subblock value over the model arg', () => {
229+
const mapped = mapToolCall('servicenow_list_incidents', { incidentState: '6', state: '2' })
230+
231+
expect(mapped.state).toBe('6')
232+
})
233+
})

‎apps/sim/blocks/blocks/servicenow.ts‎

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ServiceNowIcon } from '@/components/icons'
2-
import type { BlockConfig, BlockMeta } from '@/blocks/types'
2+
import type { BlockConfig, BlockMeta, BlockToolParamsContext } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
4-
import { normalizeFileInput, withSavedCredential } from '@/blocks/utils'
4+
import { isToolCall, normalizeFileInput, toolCallArg, withSavedCredential } from '@/blocks/utils'
55
import {
66
APPROVAL_DECISION_OPTIONS,
77
APPROVAL_STATE,
@@ -1622,9 +1622,9 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
16221622
],
16231623
config: {
16241624
tool: (params) => params.operation,
1625-
params: (params) => {
1625+
params: (params, context?: BlockToolParamsContext) => {
16261626
const {
1627-
operation,
1627+
operation: storedOperation,
16281628
fields,
16291629
readFields,
16301630
returnFields,
@@ -1644,11 +1644,23 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
16441644
knowledgeQuery,
16451645
...rest
16461646
} = params
1647+
const operation = context?.operation ?? storedOperation
1648+
/**
1649+
* A dedicated subblock's value, or on a tool call where it is blank, the
1650+
* model's arg under the tool param name. Without a model arg the subblock
1651+
* value stands exactly, so an explicit "Any" (`''`) is not replaced.
1652+
*/
1653+
const dedicatedOrToolArg = (dedicated: unknown, toolArg: unknown) =>
1654+
isToolCall(context) &&
1655+
toolArg !== undefined &&
1656+
(dedicated === undefined || dedicated === null || dedicated === '')
1657+
? toolArg
1658+
: dedicated
16471659
const isCreateOrUpdate =
16481660
operation === 'servicenow_create_record' || operation === 'servicenow_update_record'
16491661

16501662
if (SEMANTIC_DISPLAY_VALUE_OPS.has(operation)) {
1651-
rest.displayValue = semanticDisplayValue
1663+
rest.displayValue = dedicatedOrToolArg(semanticDisplayValue, rest.displayValue)
16521664
}
16531665

16541666
/**
@@ -1659,7 +1671,9 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
16591671
* query — so each gets its own subblock and is republished here for the
16601672
* operations that own it. Assign explicitly rather than conditionally:
16611673
* `finalInputs` merges this result over the raw inputs, so a key left
1662-
* off is not dropped, it keeps whatever the raw input held.
1674+
* off is not dropped, it keeps whatever the raw input held. On a tool
1675+
* call the model supplies these under the tool param names, so a blank
1676+
* dedicated subblock falls back to the model's value instead of wiping it.
16631677
*/
16641678
const stateSource =
16651679
operation === 'servicenow_update_change_state'
@@ -1669,12 +1683,24 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
16691683
: CHANGE_STATE_OPS.has(operation)
16701684
? changeState
16711685
: incidentState
1672-
rest.state = SEMANTIC_STATE_OPS.has(operation) ? stateSource : undefined
1673-
rest.comments =
1674-
operation === 'servicenow_update_approval' ? approvalComments : incidentComments
1675-
rest.closeCode = CHANGE_CLOSE_OPS.has(operation) ? changeCloseCode : resolutionCode
1676-
rest.closeNotes = CHANGE_CLOSE_OPS.has(operation) ? changeCloseNotes : resolutionNotes
1677-
if (operation === 'servicenow_search_knowledge') rest.query = knowledgeQuery
1686+
rest.state = SEMANTIC_STATE_OPS.has(operation)
1687+
? dedicatedOrToolArg(stateSource, rest.state)
1688+
: undefined
1689+
rest.comments = dedicatedOrToolArg(
1690+
operation === 'servicenow_update_approval' ? approvalComments : incidentComments,
1691+
rest.comments
1692+
)
1693+
rest.closeCode = dedicatedOrToolArg(
1694+
CHANGE_CLOSE_OPS.has(operation) ? changeCloseCode : resolutionCode,
1695+
rest.closeCode
1696+
)
1697+
rest.closeNotes = dedicatedOrToolArg(
1698+
CHANGE_CLOSE_OPS.has(operation) ? changeCloseNotes : resolutionNotes,
1699+
rest.closeNotes
1700+
)
1701+
if (operation === 'servicenow_search_knowledge') {
1702+
rest.query = dedicatedOrToolArg(knowledgeQuery, rest.query)
1703+
}
16781704

16791705
/**
16801706
* `attachmentLimit` exists as a separate subblock precisely so a limit
@@ -1684,8 +1710,8 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
16841710
* Every paginated operation reads exactly one of the two.
16851711
*/
16861712
if (operation === 'servicenow_list_attachments') {
1687-
rest.limit =
1688-
attachmentLimit != null && attachmentLimit !== '' ? Number(attachmentLimit) : undefined
1713+
const limit = dedicatedOrToolArg(attachmentLimit, rest.limit)
1714+
rest.limit = limit != null && limit !== '' ? Number(limit) : undefined
16891715
} else if (rest.limit != null) {
16901716
rest.limit = rest.limit === '' ? undefined : Number(rest.limit)
16911717
}
@@ -1741,7 +1767,12 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st
17411767
}
17421768

17431769
const projection = operation === 'servicenow_read_record' ? readFields : returnFields
1744-
return { ...rest, fields: projection || undefined }
1770+
const toolFields = toolCallArg(context, fields)
1771+
const toolProjection =
1772+
typeof toolFields === 'string' && !toolFields.trim().startsWith('{')
1773+
? toolFields
1774+
: undefined
1775+
return { ...rest, fields: projection || toolProjection || undefined }
17451776
},
17461777
},
17471778
},

‎apps/sim/tools/hubspot/add_list_memberships.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
HubSpotAddListMembershipsResponse,
55
} from '@/tools/hubspot/types'
66
import type { ToolConfig } from '@/tools/types'
7+
import { safeUrlPathSegment } from '@/tools/url-path'
78

89
const logger = createLogger('HubSpotAddListMemberships')
910

@@ -44,7 +45,8 @@ export const hubspotAddListMembershipsTool: ToolConfig<
4445
},
4546

4647
request: {
47-
url: (params) => `https://api.hubapi.com/crm/v3/lists/${params.listId.trim()}/memberships/add`,
48+
url: (params) =>
49+
`https://api.hubapi.com/crm/v3/lists/${safeUrlPathSegment(params.listId, 'listId')}/memberships/add`,
4850
method: 'PUT',
4951
headers: (params) => {
5052
if (!params.accessToken) {

‎apps/sim/tools/hubspot/create_association.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
HubSpotCreateAssociationResponse,
55
} from '@/tools/hubspot/types'
66
import type { ToolConfig } from '@/tools/types'
7+
import { safeUrlPathSegment } from '@/tools/url-path'
78

89
const logger = createLogger('HubSpotCreateAssociation')
910

@@ -71,8 +72,8 @@ export const hubspotCreateAssociationTool: ToolConfig<
7172

7273
request: {
7374
url: (params) => {
74-
const from = `${encodeURIComponent(params.objectType.trim())}/${encodeURIComponent(params.objectId.trim())}`
75-
const to = `${encodeURIComponent(params.toObjectType.trim())}/${encodeURIComponent(params.toObjectId.trim())}`
75+
const from = `${safeUrlPathSegment(params.objectType, 'objectType')}/${safeUrlPathSegment(params.objectId, 'objectId')}`
76+
const to = `${safeUrlPathSegment(params.toObjectType, 'toObjectType')}/${safeUrlPathSegment(params.toObjectId, 'toObjectId')}`
7677

7778
return params.associationTypeId != null
7879
? `https://api.hubapi.com/crm/v4/objects/${from}/associations/${to}`

‎apps/sim/tools/hubspot/delete_association.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
HubSpotDeleteAssociationResponse,
55
} from '@/tools/hubspot/types'
66
import type { ToolConfig } from '@/tools/types'
7+
import { safeUrlPathSegment } from '@/tools/url-path'
78

89
const logger = createLogger('HubSpotDeleteAssociation')
910

@@ -56,8 +57,8 @@ export const hubspotDeleteAssociationTool: ToolConfig<
5657

5758
request: {
5859
url: (params) => {
59-
const from = `${encodeURIComponent(params.objectType.trim())}/${encodeURIComponent(params.objectId.trim())}`
60-
const to = `${encodeURIComponent(params.toObjectType.trim())}/${encodeURIComponent(params.toObjectId.trim())}`
60+
const from = `${safeUrlPathSegment(params.objectType, 'objectType')}/${safeUrlPathSegment(params.objectId, 'objectId')}`
61+
const to = `${safeUrlPathSegment(params.toObjectType, 'toObjectType')}/${safeUrlPathSegment(params.toObjectId, 'toObjectId')}`
6162
return `https://api.hubapi.com/crm/v4/objects/${from}/associations/${to}`
6263
},
6364
method: 'DELETE',

‎apps/sim/tools/hubspot/delete_company.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
HubSpotDeleteCompanyResponse,
55
} from '@/tools/hubspot/types'
66
import type { ToolConfig } from '@/tools/types'
7+
import { safeUrlPathSegment } from '@/tools/url-path'
78

89
const logger = createLogger('HubSpotDeleteCompany')
910

@@ -37,7 +38,8 @@ export const hubspotDeleteCompanyTool: ToolConfig<
3738
},
3839

3940
request: {
40-
url: (params) => `https://api.hubapi.com/crm/v3/objects/companies/${params.companyId.trim()}`,
41+
url: (params) =>
42+
`https://api.hubapi.com/crm/v3/objects/companies/${safeUrlPathSegment(params.companyId, 'companyId')}`,
4143
method: 'DELETE',
4244
headers: (params) => {
4345
if (!params.accessToken) {

‎apps/sim/tools/hubspot/delete_contact.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
HubSpotDeleteContactResponse,
55
} from '@/tools/hubspot/types'
66
import type { ToolConfig } from '@/tools/types'
7+
import { safeUrlPathSegment } from '@/tools/url-path'
78

89
const logger = createLogger('HubSpotDeleteContact')
910

@@ -37,7 +38,8 @@ export const hubspotDeleteContactTool: ToolConfig<
3738
},
3839

3940
request: {
40-
url: (params) => `https://api.hubapi.com/crm/v3/objects/contacts/${params.contactId.trim()}`,
41+
url: (params) =>
42+
`https://api.hubapi.com/crm/v3/objects/contacts/${safeUrlPathSegment(params.contactId, 'contactId')}`,
4143
method: 'DELETE',
4244
headers: (params) => {
4345
if (!params.accessToken) {

‎apps/sim/tools/hubspot/delete_deal.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import type { HubSpotDeleteDealParams, HubSpotDeleteDealResponse } from '@/tools/hubspot/types'
33
import type { ToolConfig } from '@/tools/types'
4+
import { safeUrlPathSegment } from '@/tools/url-path'
45

56
const logger = createLogger('HubSpotDeleteDeal')
67

@@ -32,7 +33,8 @@ export const hubspotDeleteDealTool: ToolConfig<HubSpotDeleteDealParams, HubSpotD
3233
},
3334

3435
request: {
35-
url: (params) => `https://api.hubapi.com/crm/v3/objects/deals/${params.dealId.trim()}`,
36+
url: (params) =>
37+
`https://api.hubapi.com/crm/v3/objects/deals/${safeUrlPathSegment(params.dealId, 'dealId')}`,
3638
method: 'DELETE',
3739
headers: (params) => {
3840
if (!params.accessToken) {

‎apps/sim/tools/hubspot/delete_line_item.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
HubSpotDeleteLineItemResponse,
55
} from '@/tools/hubspot/types'
66
import type { ToolConfig } from '@/tools/types'
7+
import { safeUrlPathSegment } from '@/tools/url-path'
78

89
const logger = createLogger('HubSpotDeleteLineItem')
910

@@ -37,7 +38,8 @@ export const hubspotDeleteLineItemTool: ToolConfig<
3738
},
3839

3940
request: {
40-
url: (params) => `https://api.hubapi.com/crm/v3/objects/line_items/${params.lineItemId.trim()}`,
41+
url: (params) =>
42+
`https://api.hubapi.com/crm/v3/objects/line_items/${safeUrlPathSegment(params.lineItemId, 'lineItemId')}`,
4143
method: 'DELETE',
4244
headers: (params) => {
4345
if (!params.accessToken) {

0 commit comments

Comments
 (0)