From 287603d5e909fb0417c9bcffce4021d1d9f8a883 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 3 Sep 2026 18:32:05 -0700 Subject: [PATCH 01/20] feat(oracle-epm): add guarded foundation --- .../sim/lib/api/contracts/credentials.test.ts | 22 + .../descriptors.test.ts | 10 + .../client-credential-accounts/descriptors.ts | 33 ++ .../minters/oracle-epm.test.ts | 55 +++ .../minters/oracle-epm.ts | 76 +++ .../client-credential-accounts/server.test.ts | 27 +- .../client-credential-accounts/server.ts | 11 +- .../credentials/orchestration/index.test.ts | 32 ++ .../service-account-provider-ids.test.ts | 2 + .../service-account-secret.test.ts | 36 +- .../internal/oracle-epm/client.server.test.ts | 321 +++++++++++++ .../lib/internal/oracle-epm/client.server.ts | 443 ++++++++++++++++++ .../internal/oracle-epm/destination.test.ts | 40 ++ .../lib/internal/oracle-epm/destination.ts | 89 ++++ .../lib/internal/oracle-epm/endpoint.test.ts | 93 ++++ apps/sim/lib/internal/oracle-epm/endpoint.ts | 292 ++++++++++++ .../lib/internal/oracle-epm/errors.test.ts | 70 +++ apps/sim/lib/internal/oracle-epm/errors.ts | 149 ++++++ .../internal/oracle-epm/files.server.test.ts | 258 ++++++++++ .../lib/internal/oracle-epm/files.server.ts | 194 ++++++++ apps/sim/lib/internal/oracle-epm/index.ts | 37 ++ apps/sim/lib/internal/oracle-epm/jobs.test.ts | 94 ++++ apps/sim/lib/internal/oracle-epm/jobs.ts | 126 +++++ .../sim/lib/internal/oracle-epm/links.test.ts | 60 +++ apps/sim/lib/internal/oracle-epm/links.ts | 125 +++++ .../internal/oracle-epm/route-space.test.ts | 40 ++ .../lib/internal/oracle-epm/route-space.ts | 74 +++ apps/sim/lib/internal/oracle-epm/types.ts | 164 +++++++ apps/sim/lib/oauth/credential-service.test.ts | 81 +++- apps/sim/lib/oauth/token-resolution.test.ts | 109 +++++ apps/sim/lib/oauth/token-resolution.ts | 73 ++- apps/sim/tools/shared/oracle-epm.test.ts | 38 ++ apps/sim/tools/shared/oracle-epm.ts | 60 +++ 33 files changed, 3320 insertions(+), 14 deletions(-) create mode 100644 apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.test.ts create mode 100644 apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.ts create mode 100644 apps/sim/lib/internal/oracle-epm/client.server.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm/client.server.ts create mode 100644 apps/sim/lib/internal/oracle-epm/destination.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm/destination.ts create mode 100644 apps/sim/lib/internal/oracle-epm/endpoint.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm/endpoint.ts create mode 100644 apps/sim/lib/internal/oracle-epm/errors.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm/errors.ts create mode 100644 apps/sim/lib/internal/oracle-epm/files.server.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm/files.server.ts create mode 100644 apps/sim/lib/internal/oracle-epm/index.ts create mode 100644 apps/sim/lib/internal/oracle-epm/jobs.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm/jobs.ts create mode 100644 apps/sim/lib/internal/oracle-epm/links.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm/links.ts create mode 100644 apps/sim/lib/internal/oracle-epm/route-space.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm/route-space.ts create mode 100644 apps/sim/lib/internal/oracle-epm/types.ts create mode 100644 apps/sim/tools/shared/oracle-epm.test.ts create mode 100644 apps/sim/tools/shared/oracle-epm.ts diff --git a/apps/sim/lib/api/contracts/credentials.test.ts b/apps/sim/lib/api/contracts/credentials.test.ts index ae6f6406ee7..38749d0da24 100644 --- a/apps/sim/lib/api/contracts/credentials.test.ts +++ b/apps/sim/lib/api/contracts/credentials.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it } from 'vitest' import { + createCredentialBodySchema, updateCredentialByIdBodySchema, workspaceCredentialSchema, } from '@/lib/api/contracts/credentials' @@ -46,3 +47,24 @@ describe('workspaceCredentialSchema unredacted', () => { expect(workspaceCredentialSchema.safeParse(credential).success).toBe(false) }) }) + +describe('Oracle EPM service-account credential contract', () => { + const valid = { + workspaceId: '00000000-0000-4000-8000-000000000001', + type: 'service_account' as const, + providerId: 'oracle-epm-service-account', + orgId: 'https://epm.example.com/gateway', + clientId: 'integration.user@example.com', + clientSecret: 'password', + } + + it('accepts the descriptor-required integration-user fields', () => { + expect(createCredentialBodySchema.safeParse(valid).success).toBe(true) + }) + + it.each(['orgId', 'clientId', 'clientSecret'] as const)('rejects a missing %s', (field) => { + expect(createCredentialBodySchema.safeParse({ ...valid, [field]: undefined }).success).toBe( + false + ) + }) +}) diff --git a/apps/sim/lib/credentials/client-credential-accounts/descriptors.test.ts b/apps/sim/lib/credentials/client-credential-accounts/descriptors.test.ts index 5ea3e35e79d..ca6400765c8 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/descriptors.test.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/descriptors.test.ts @@ -7,6 +7,7 @@ import { getClientCredentialAccountDescriptor, NETSUITE_SERVICE_ACCOUNT_PROVIDER_ID, normalizeNetSuiteSuiteTalkOrigin, + ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID, partitionClientCredentialFields, resolveClientCredentialAuthMethod, resolveSalesforceAuthMethod, @@ -19,6 +20,7 @@ const salesforce = getClientCredentialAccountDescriptor(SALESFORCE_SERVICE_ACCOU const box = getClientCredentialAccountDescriptor(BOX_SERVICE_ACCOUNT_PROVIDER_ID)! const zohoDesk = getClientCredentialAccountDescriptor(ZOHO_DESK_SERVICE_ACCOUNT_PROVIDER_ID)! const netSuite = getClientCredentialAccountDescriptor(NETSUITE_SERVICE_ACCOUNT_PROVIDER_ID)! +const oracleEpm = getClientCredentialAccountDescriptor(ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID)! const ids = (fields: { id: string }[]) => fields.map((field) => field.id) @@ -51,6 +53,14 @@ describe('partitionClientCredentialFields', () => { multiline: true, }) }) + + it('declares the Oracle EPM integration user without a UI-specific implementation', () => { + const { visible, required } = partitionClientCredentialFields(oracleEpm, undefined) + expect(ids(visible)).toEqual(['orgId', 'clientId', 'clientSecret']) + expect(ids(required)).toEqual(['orgId', 'clientId', 'clientSecret']) + expect(oracleEpm.connectNoun).toBe('integration user') + expect(oracleEpm.fields.find((field) => field.id === 'clientSecret')?.secret).toBe(true) + }) }) describe('Salesforce, which offers two grants', () => { diff --git a/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts b/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts index dcd4aa26f7d..7fd4f83abb2 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts @@ -111,6 +111,7 @@ export const BOX_SERVICE_ACCOUNT_PROVIDER_ID = 'box-service-account' as const export const SALESFORCE_SERVICE_ACCOUNT_PROVIDER_ID = 'salesforce-service-account' as const export const ZOHO_DESK_SERVICE_ACCOUNT_PROVIDER_ID = 'zoho-desk-service-account' as const export const NETSUITE_SERVICE_ACCOUNT_PROVIDER_ID = 'netsuite-service-account' as const +export const ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID = 'oracle-epm-service-account' as const export type ClientCredentialAccountProviderId = | typeof ZOOM_SERVICE_ACCOUNT_PROVIDER_ID @@ -118,6 +119,7 @@ export type ClientCredentialAccountProviderId = | typeof SALESFORCE_SERVICE_ACCOUNT_PROVIDER_ID | typeof ZOHO_DESK_SERVICE_ACCOUNT_PROVIDER_ID | typeof NETSUITE_SERVICE_ACCOUNT_PROVIDER_ID + | typeof ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID /** * Exact account-specific SuiteTalk origin accepted by NetSuite's OAuth and @@ -531,6 +533,37 @@ export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record< helpText: 'Use the account-specific SuiteTalk URL and the client ID, certificate ID, and private key from one OAuth 2.0 client-credentials mapping.', }, + [ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID]: { + providerId: ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID, + serviceLabel: 'Oracle EPM Cloud', + connectNoun: 'integration user', + fields: [ + { + id: 'orgId', + label: 'Environment URL', + placeholder: 'https://example.oraclecloud.com/epmcloud', + secret: false, + hintPattern: /^https:\/\//, + hintMessage: 'Expected the full HTTPS URL for one Oracle EPM environment.', + }, + { + id: 'clientId', + label: 'Integration username', + placeholder: 'integration.user@example.com', + secret: false, + }, + { + id: 'clientSecret', + label: 'Password', + placeholder: 'Paste the integration user password', + secret: true, + }, + ], + docsUrl: + 'https://docs.oracle.com/en/cloud/saas/enterprise-performance-management-common/prest/overview.html', + helpText: + 'The credential is bound to one EPM environment. Use a dedicated integration user with only the permissions its workflows require.', + }, } /** diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.test.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.test.ts new file mode 100644 index 00000000000..38f3ae3dd91 --- /dev/null +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.test.ts @@ -0,0 +1,55 @@ +/** @vitest-environment node */ +import { describe, expect, it, vi } from 'vitest' +import { mintOracleEpmServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/oracle-epm' +import { TokenServiceAccountValidationError } from '@/lib/credentials/token-service-accounts/errors' + +describe('mintOracleEpmServiceAccountToken', () => { + it('mints Basic authentication locally and binds the normalized destination', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch') + const result = await mintOracleEpmServiceAccountToken({ + orgId: ' https://EPM.example.com/gateway/ ', + clientId: 'integration.user@example.com', + clientSecret: 'password', + }) + expect(Buffer.from(result.accessToken, 'base64').toString()).toBe( + 'integration.user@example.com:password' + ) + expect(result).toMatchObject({ + expiresInSeconds: 600, + instanceUrl: 'https://epm.example.com/gateway', + identity: { + principal: null, + auditMetadata: { environmentUrl: 'https://epm.example.com/gateway' }, + storedMetadata: { environmentUrl: 'https://epm.example.com/gateway' }, + }, + }) + expect(JSON.stringify(result.identity)).not.toContain('password') + expect(JSON.stringify(result.identity)).not.toContain('integration.user') + expect(fetchSpy).not.toHaveBeenCalled() + fetchSpy.mockRestore() + }) + + it.each([ + { clientId: 'user:name', clientSecret: 'password' }, + { clientId: 'user\nname', clientSecret: 'password' }, + { clientId: 'user', clientSecret: 'pass\nword' }, + { clientId: '', clientSecret: 'password' }, + ])('rejects unsafe Basic credential text', async (credentials) => { + await expect( + mintOracleEpmServiceAccountToken({ + orgId: 'https://epm.example.com', + ...credentials, + }) + ).rejects.toBeInstanceOf(TokenServiceAccountValidationError) + }) + + it('does not reflect secrets in validation errors', async () => { + const secret = 'password-with-newline\n' + const error = await mintOracleEpmServiceAccountToken({ + orgId: 'https://epm.example.com', + clientId: 'user', + clientSecret: secret, + }).catch((value: unknown) => value) + expect(JSON.stringify(error)).not.toContain(secret) + }) +}) diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.ts new file mode 100644 index 00000000000..fce8eb4529e --- /dev/null +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.ts @@ -0,0 +1,76 @@ +import type { + ClientCredentialAccountFields, + ClientCredentialAccountMintOptions, + ClientCredentialAccountMintResult, +} from '@/lib/credentials/client-credential-accounts/server' +import { + requireClientSecret, + TokenServiceAccountValidationError, +} from '@/lib/credentials/token-service-accounts/errors' +import { normalizeOracleEpmDestination } from '@/lib/internal/oracle-epm/destination' + +const SYNTHETIC_TOKEN_TTL_SECONDS = 600 +const MAX_USERNAME_BYTES = 255 +const MAX_AUTH_VALUE_BYTES = 1_024 +const FORBIDDEN_CREDENTIAL_TEXT = /[\u0000-\u001f\u007f]/ + +function invalidCredentials(reason: string): TokenServiceAccountValidationError { + return new TokenServiceAccountValidationError('invalid_credentials', 400, { + step: 'oracle_epm_basic_auth', + reason, + }) +} + +/** + * Builds credential-bound Basic authentication locally. Oracle EPM does not + * expose a token mint for this v1 flow, so connect performs no network probe. + */ +export async function mintOracleEpmServiceAccountToken( + fields: ClientCredentialAccountFields, + _options?: ClientCredentialAccountMintOptions +): Promise { + let instanceUrl: string + try { + instanceUrl = normalizeOracleEpmDestination(fields.orgId) + } catch { + throw new TokenServiceAccountValidationError('site_not_found', 400, { + step: 'oracle_epm_destination_validation', + reason: 'environment URL must be a valid HTTPS Oracle EPM destination', + }) + } + + const username = fields.clientId.trim() + const password = requireClientSecret( + fields.clientSecret, + 'oracle_epm_basic_auth', + 'Oracle EPM Cloud' + ) + if ( + !username || + username.includes(':') || + FORBIDDEN_CREDENTIAL_TEXT.test(username) || + Buffer.byteLength(username, 'utf8') > MAX_USERNAME_BYTES + ) { + throw invalidCredentials('integration username is invalid') + } + if ( + !password || + FORBIDDEN_CREDENTIAL_TEXT.test(password) || + Buffer.byteLength(password, 'utf8') > MAX_AUTH_VALUE_BYTES + ) { + throw invalidCredentials('password is invalid') + } + + const hostname = new URL(instanceUrl).hostname + return { + accessToken: Buffer.from(`${username}:${password}`, 'utf8').toString('base64'), + expiresInSeconds: SYNTHETIC_TOKEN_TTL_SECONDS, + instanceUrl, + identity: { + displayName: `Oracle EPM ${hostname}`, + principal: null, + auditMetadata: { environmentUrl: instanceUrl }, + storedMetadata: { environmentUrl: instanceUrl }, + }, + } +} diff --git a/apps/sim/lib/credentials/client-credential-accounts/server.test.ts b/apps/sim/lib/credentials/client-credential-accounts/server.test.ts index c91df17c197..1a5a989aa42 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/server.test.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/server.test.ts @@ -3,7 +3,10 @@ */ import { describe, expect, it } from 'vitest' import { CLIENT_CREDENTIAL_ACCOUNT_SECRET_TYPE } from '@/lib/credentials/client-credential-accounts/descriptors' -import { parseClientCredentialAccountSecretBlob } from '@/lib/credentials/client-credential-accounts/server' +import { + getClientCredentialAccountMinter, + parseClientCredentialAccountSecretBlob, +} from '@/lib/credentials/client-credential-accounts/server' const MALFORMED = 'Stored client-credential service-account secret is malformed' @@ -19,6 +22,10 @@ function blob(overrides: Record = {}): string { } describe('parseClientCredentialAccountSecretBlob', () => { + it('registers the Oracle EPM minter in the generic client-credential pipeline', () => { + expect(getClientCredentialAccountMinter('oracle-epm-service-account')).toBeTypeOf('function') + }) + it('returns the parsed blob when it matches the expected provider', () => { const parsed = parseClientCredentialAccountSecretBlob(blob(), 'zoom-service-account') expect(parsed.clientId).toBe('cid') @@ -117,4 +124,22 @@ describe('parseClientCredentialAccountSecretBlob', () => { ) ).toThrow(MALFORMED) }) + + it('requires the complete Oracle EPM integration-user blob', () => { + const oracleBlob = blob({ + providerId: 'oracle-epm-service-account', + orgId: 'https://epm.example.com/gateway', + clientId: 'integration.user@example.com', + clientSecret: 'password', + }) + expect( + parseClientCredentialAccountSecretBlob(oracleBlob, 'oracle-epm-service-account') + ).toMatchObject({ orgId: 'https://epm.example.com/gateway' }) + expect(() => + parseClientCredentialAccountSecretBlob( + blob({ providerId: 'oracle-epm-service-account', clientSecret: '' }), + 'oracle-epm-service-account' + ) + ).toThrow(MALFORMED) + }) }) diff --git a/apps/sim/lib/credentials/client-credential-accounts/server.ts b/apps/sim/lib/credentials/client-credential-accounts/server.ts index 10c829c7861..28e15295bab 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/server.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/server.ts @@ -5,6 +5,7 @@ import { getClientCredentialAccountDescriptor, isClientCredentialAccountProviderId, NETSUITE_SERVICE_ACCOUNT_PROVIDER_ID, + ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID, partitionClientCredentialFields, SALESFORCE_SERVICE_ACCOUNT_PROVIDER_ID, ZOHO_DESK_SERVICE_ACCOUNT_PROVIDER_ID, @@ -12,6 +13,7 @@ import { } from '@/lib/credentials/client-credential-accounts/descriptors' import { mintBoxServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/box' import { mintNetSuiteServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/netsuite' +import { mintOracleEpmServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/oracle-epm' import { mintSalesforceServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/salesforce' import { mintZohoDeskServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/zoho-desk' import { mintZoomServiceAccountToken } from '@/lib/credentials/client-credential-accounts/minters/zoom' @@ -29,8 +31,8 @@ export interface ClientCredentialAccountFields { clientSecret?: string /** * Provider-specific org identifier (Zoom Account ID, Box Enterprise ID, - * Salesforce My Domain host, Zoho Desk organization ID, or NetSuite - * SuiteTalk origin). + * Salesforce My Domain host, Zoho Desk organization ID, NetSuite SuiteTalk + * origin, or an Oracle EPM environment URL). */ orgId: string /** @@ -84,8 +86,8 @@ export interface ClientCredentialAccountMintResult { accessToken: string expiresInSeconds: number /** - * Provider API origin the minted token must be used against (Salesforce or - * NetSuite), forwarded to tools alongside the token. + * Provider API destination the minted token must be used against (Salesforce, + * NetSuite, or Oracle EPM), forwarded to tools alongside the token. */ instanceUrl?: string /** @@ -130,6 +132,7 @@ const CLIENT_CREDENTIAL_ACCOUNT_MINTERS: Record< [SALESFORCE_SERVICE_ACCOUNT_PROVIDER_ID]: mintSalesforceServiceAccountToken, [ZOHO_DESK_SERVICE_ACCOUNT_PROVIDER_ID]: mintZohoDeskServiceAccountToken, [NETSUITE_SERVICE_ACCOUNT_PROVIDER_ID]: mintNetSuiteServiceAccountToken, + [ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID]: mintOracleEpmServiceAccountToken, } export function getClientCredentialAccountMinter( diff --git a/apps/sim/lib/credentials/orchestration/index.test.ts b/apps/sim/lib/credentials/orchestration/index.test.ts index 54510cd4902..f702180f0d0 100644 --- a/apps/sim/lib/credentials/orchestration/index.test.ts +++ b/apps/sim/lib/credentials/orchestration/index.test.ts @@ -414,6 +414,38 @@ describe('performUpdateCredential — service-account secret rotation', () => { ) }) + it('threads Oracle EPM integration-user fields through reconnect without reading old secrets', async () => { + mockCredential({ + providerId: 'oracle-epm-service-account', + displayName: 'Production EPM', + }) + mockIsClientCredentialAccountProviderId.mockReturnValue(true) + mockVerifyAndBuildServiceAccountSecret.mockResolvedValue({ + providerId: 'oracle-epm-service-account', + encryptedServiceAccountKey: 'new-cipher', + displayName: 'Production EPM', + auditMetadata: {}, + }) + + await performUpdateCredential({ + credentialId: 'cred-1', + userId: 'user-1', + orgId: 'https://epm.example.com/gateway', + clientId: 'integration.user@example.com', + clientSecret: 'rotated-password', + }) + + expect(mockDecryptSecret).not.toHaveBeenCalled() + expect(mockVerifyAndBuildServiceAccountSecret).toHaveBeenCalledWith( + 'oracle-epm-service-account', + expect.objectContaining({ + orgId: 'https://epm.example.com/gateway', + clientId: 'integration.user@example.com', + clientSecret: 'rotated-password', + }) + ) + }) + it('surfaces a rebuild failure as a validation error and writes nothing', async () => { mockCredential() mockStoredBlob({ type: 'service_account', client_email: OLD_EMAIL }) diff --git a/apps/sim/lib/credentials/service-account-provider-ids.test.ts b/apps/sim/lib/credentials/service-account-provider-ids.test.ts index 19fa62966df..7cab5ccf5de 100644 --- a/apps/sim/lib/credentials/service-account-provider-ids.test.ts +++ b/apps/sim/lib/credentials/service-account-provider-ids.test.ts @@ -16,6 +16,7 @@ describe('isServiceAccountProviderId', () => { expect(isServiceAccountProviderId('notion-service-account')).toBe(true) expect(isServiceAccountProviderId('salesforce-service-account')).toBe(true) expect(isServiceAccountProviderId('netsuite-service-account')).toBe(true) + expect(isServiceAccountProviderId('oracle-epm-service-account')).toBe(true) }) it('is case- and whitespace-insensitive', () => { @@ -52,6 +53,7 @@ describe('getServiceAccountConnectNoun', () => { it('names the client-credential secret', () => { expect(getServiceAccountConnectNoun('zoom-service-account')).toBe('server-to-server app') expect(getServiceAccountConnectNoun('netsuite-service-account')).toBe('OAuth certificate') + expect(getServiceAccountConnectNoun('oracle-epm-service-account')).toBe('integration user') }) it('calls a custom Slack bot a custom bot', () => { diff --git a/apps/sim/lib/credentials/service-account-secret.test.ts b/apps/sim/lib/credentials/service-account-secret.test.ts index fd11efb6b33..49bfe6a00fd 100644 --- a/apps/sim/lib/credentials/service-account-secret.test.ts +++ b/apps/sim/lib/credentials/service-account-secret.test.ts @@ -43,7 +43,8 @@ vi.mock('@/lib/credentials/client-credential-accounts/server', () => ({ getClientCredentialAccountMinter: (providerId: string) => providerId === 'zoom-service-account' || providerId === 'box-service-account' || - providerId === 'netsuite-service-account' + providerId === 'netsuite-service-account' || + providerId === 'oracle-epm-service-account' ? mockClientCredentialMinter : undefined, })) @@ -261,6 +262,39 @@ describe('verifyAndBuildServiceAccountSecret', () => { }) }) + it('stores the Oracle EPM environment and integration-user secret through the generic path', async () => { + mockClientCredentialMinter.mockResolvedValue({ + accessToken: 'basic-token', + expiresInSeconds: 600, + instanceUrl: 'https://epm.example.com/gateway', + identity: { + displayName: 'Oracle EPM epm.example.com', + principal: null, + auditMetadata: { environmentUrl: 'https://epm.example.com/gateway' }, + storedMetadata: { environmentUrl: 'https://epm.example.com/gateway' }, + }, + }) + const result = await verifyAndBuildServiceAccountSecret('oracle-epm-service-account', { + orgId: ' https://epm.example.com/gateway ', + clientId: ' integration.user@example.com ', + clientSecret: ' password ', + }) + + expect(mockClientCredentialMinter).toHaveBeenCalledWith({ + orgId: 'https://epm.example.com/gateway', + clientId: 'integration.user@example.com', + clientSecret: 'password', + }) + expect(JSON.parse(result.encryptedServiceAccountKey)).toMatchObject({ + providerId: 'oracle-epm-service-account', + orgId: 'https://epm.example.com/gateway', + clientId: 'integration.user@example.com', + clientSecret: 'password', + metadata: { environmentUrl: 'https://epm.example.com/gateway' }, + }) + expect(result.principal).toBeNull() + }) + it('throws when client-credential required fields are missing, without minting', async () => { await expect( verifyAndBuildServiceAccountSecret('zoom-service-account', { diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts new file mode 100644 index 00000000000..54d33bfe890 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -0,0 +1,321 @@ +/** @vitest-environment node */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockSecureFetch, mockValidateUrl } = vi.hoisted(() => ({ + mockSecureFetch: vi.fn(), + mockValidateUrl: vi.fn(), +})) + +vi.mock('@/lib/core/security/input-validation.server', () => ({ + DEFAULT_MAX_RESPONSE_BYTES: 100 * 1024 * 1024, + secureFetchWithPinnedIP: mockSecureFetch, + validateUrlWithDNS: mockValidateUrl, +})) + +import { createOracleEpmClient } from '@/lib/internal/oracle-epm/client.server' +import { + oracleEpmLiteral, + oracleEpmPathParameter, + oracleEpmQuery, +} from '@/lib/internal/oracle-epm/endpoint' +import { OracleEpmError } from '@/lib/internal/oracle-epm/errors' +import { defineOracleEpmRouteSpace } from '@/lib/internal/oracle-epm/route-space' +import type { OracleEpmValidatedLink } from '@/lib/internal/oracle-epm/types' + +const routes = defineOracleEpmRouteSpace({ + context: ['SyntheticAlpha', 'rest'], + allowedVersions: ['v3'], +}) +const getJob = routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('jobs'), oracleEpmPathParameter('jobId', { maxBytes: 64 })], + query: { limit: oracleEpmQuery.integer({ minimum: 1, maximum: 100 }) }, + headers: { etag: { name: 'If-None-Match', maxBytes: 128 } }, + body: 'none', + response: 'json', + timeoutMs: 5_000, + maxResponseBytes: 4_096, + errors: { + providerCodePath: ['code'], + allowedProviderCodes: ['KNOWN'], + correlationHeaders: ['x-request-id'], + }, +}) + +function secureResponse(input: { + ok?: boolean + status?: number + data?: unknown + body?: ReadableStream | null +}) { + const data = input.data ?? { ok: true } + const defaultBody = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(JSON.stringify(data))) + controller.close() + }, + }) + return { + ok: input.ok ?? true, + status: input.status ?? 200, + statusText: '', + headers: { get: (name: string) => (name === 'x-request-id' ? 'request-1' : null) }, + body: input.body === undefined ? defaultBody : input.body, + text: async () => JSON.stringify(data), + json: async () => data, + arrayBuffer: async () => new ArrayBuffer(0), + } +} + +describe('Oracle EPM guarded client', () => { + beforeEach(() => { + vi.clearAllMocks() + mockValidateUrl.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' }) + mockSecureFetch.mockResolvedValue(secureResponse({})) + }) + + it('binds an encoded request to the credential origin and gateway path', async () => { + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway/acme', + accessToken: Buffer.from('user:password').toString('base64'), + }) + await client.request(getJob, { + pathParams: { jobId: 'job with spaces' }, + query: { limit: 25 }, + headers: { etag: 'safe-etag' }, + }) + + expect(mockValidateUrl).toHaveBeenCalledWith( + 'https://epm.example.com', + 'Oracle EPM destination', + 'configuredEndpoint', + { logDetails: false } + ) + expect(mockSecureFetch).toHaveBeenCalledWith( + expect.any(String), + '203.0.113.10', + expect.objectContaining({ + method: 'GET', + maxRedirects: 0, + headers: expect.objectContaining({ + Authorization: expect.stringMatching(/^Basic /), + 'If-None-Match': 'safe-etag', + }), + }) + ) + }) + + it.each([ + { pathParams: { jobId: '../admin' } }, + { pathParams: { jobId: 'ok' }, query: { unknown: 'value' } }, + { pathParams: { jobId: 'ok' }, query: { limit: 101 } }, + { pathParams: { jobId: 'ok' }, headers: { Authorization: 'forged' } }, + ])('rejects undeclared or out-of-bounds request input', async (input) => { + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/base', + accessToken: Buffer.from('user:password').toString('base64'), + }) + await expect(client.request(getJob, input)).rejects.toBeInstanceOf(OracleEpmError) + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + + it('encodes already-encoded traversal text as one inert path segment', async () => { + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await client.request(getJob, { pathParams: { jobId: '%2e%2e%2fadmin' } }) + expect(mockValidateUrl).toHaveBeenCalledWith( + 'https://epm.example.com', + expect.any(String), + 'configuredEndpoint', + expect.any(Object) + ) + expect(mockSecureFetch.mock.calls[0][0]).toContain('%252e%252e%252fadmin') + }) + + it('suppresses arbitrary provider bodies in failed requests', async () => { + mockSecureFetch.mockResolvedValue( + secureResponse({ + ok: false, + status: 400, + data: { code: 'UNKNOWN', message: 'secret-provider-message' }, + }) + ) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('user:password').toString('base64'), + }) + const error = await client + .request(getJob, { pathParams: { jobId: '42' } }) + .catch((value: unknown) => value) + expect(error).toBeInstanceOf(OracleEpmError) + expect(JSON.stringify(error)).not.toContain('secret-provider-message') + expect(error).toMatchObject({ providerCode: undefined, correlationId: 'request-1' }) + }) + + it('retries only a statically declared safe operation and preserves request bounds', async () => { + const endpoint = routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('health')], + body: 'none', + response: 'json', + timeoutMs: 2_000, + maxResponseBytes: 1_024, + retry: { maxAttempts: 2, statuses: [503], initialDelayMs: 1, maxDelayMs: 1 }, + }) + mockSecureFetch + .mockResolvedValueOnce(secureResponse({ ok: false, status: 503 })) + .mockResolvedValueOnce(secureResponse({ data: { ready: true } })) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await expect(client.request(endpoint)).resolves.toMatchObject({ data: { ready: true } }) + expect(mockSecureFetch).toHaveBeenCalledTimes(2) + }) + + it('rejects oversized declared request bodies before DNS or network access', async () => { + const endpoint = routes.defineEndpoint({ + method: 'POST', + version: 'v3', + path: [oracleEpmLiteral('jobs')], + body: 'json', + maxRequestBytes: 8, + response: 'json', + timeoutMs: 2_000, + maxResponseBytes: 1_024, + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + const error = await client + .request(endpoint, { json: { tooLarge: true } }) + .catch((value: unknown) => value) + expect(error).toMatchObject({ category: 'payload_too_large' }) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + + it('propagates caller aborts before opening a pinned request', async () => { + const controller = new AbortController() + controller.abort(new DOMException('user', 'AbortError')) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await expect( + client.request(getJob, { + pathParams: { jobId: '42' }, + signal: controller.signal, + }) + ).rejects.toMatchObject({ name: 'AbortError' }) + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + + it('returns an opaque same-client link capability and keeps query secrets out of serialization', async () => { + const download = routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], + query: { token: oracleEpmQuery.string({ required: true, maxBytes: 128 }) }, + body: 'none', + response: 'stream', + timeoutMs: 5_000, + maxResponseBytes: 4_096, + }) + const policy = routes.defineReturnedLinkPolicy({ + relation: 'download', + method: 'GET', + endpoint: download, + preserveGatewayBasePath: true, + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('user:password').toString('base64'), + }) + const secret = 'signed-query-secret' + const link = client.validateReturnedLink(policy, { + rel: 'download', + href: `https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=${secret}`, + }) + + expect(Object.isFrozen(link)).toBe(true) + expect(Object.keys(link)).toEqual([]) + expect(JSON.stringify(link)).toBe('{}') + expect(String(link)).not.toContain(secret) + + mockSecureFetch.mockResolvedValue(secureResponse({ body: new ReadableStream() })) + await client.requestValidatedLink(link) + expect(mockSecureFetch).toHaveBeenCalledTimes(1) + + const otherClient = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('other:password').toString('base64'), + }) + await expect(otherClient.requestValidatedLink(link)).rejects.toBeInstanceOf(OracleEpmError) + }) + + it.each([ + 'https://evil.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://user@epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://epm.example.com/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x&token=y', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?unknown=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#fragment', + ])('rejects unsafe returned link %j', (href) => { + const policy = routes.defineReturnedLinkPolicy({ + relation: 'download', + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], + query: { token: oracleEpmQuery.string({ required: true, maxBytes: 128 }) }, + response: 'stream', + timeoutMs: 5_000, + maxResponseBytes: 4_096, + preserveGatewayBasePath: true, + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('u:p').toString('base64'), + }) + expect(() => client.validateReturnedLink(policy, { rel: 'download', href })).toThrow() + }) + + it('rejects an incorrect returned-link method', () => { + const policy = routes.defineReturnedLinkPolicy({ + relation: 'download', + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], + response: 'stream', + timeoutMs: 5_000, + maxResponseBytes: 4_096, + preserveGatewayBasePath: true, + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('u:p').toString('base64'), + }) + expect(() => + client.validateReturnedLink(policy, { + rel: 'download', + method: 'POST', + href: 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc', + }) + ).toThrow() + }) + + it('rejects forged validated-link handles', async () => { + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await expect(client.requestValidatedLink({} as OracleEpmValidatedLink)).rejects.toBeInstanceOf( + OracleEpmError + ) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts new file mode 100644 index 00000000000..dafa2e64553 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -0,0 +1,443 @@ +import { interruptibleSleep } from '@sim/utils/helpers' +import { backoffWithJitter } from '@sim/utils/retry' +import { + type SecureFetchResponse, + secureFetchWithPinnedIP, + validateUrlWithDNS, +} from '@/lib/core/security/input-validation.server' +import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { + defineOracleEpmDestination, + getOracleEpmDestination, +} from '@/lib/internal/oracle-epm/destination' +import { + getOracleEpmEndpoint, + type OracleEpmEndpointDefinition, +} from '@/lib/internal/oracle-epm/endpoint' +import { + oracleEpmErrorFromResponse, + oracleEpmLocalError, + validateOracleEpmCorrelationId, +} from '@/lib/internal/oracle-epm/errors' +import { + getOracleEpmReturnedLinkPolicy, + type OracleEpmReturnedLinkPolicyDefinition, +} from '@/lib/internal/oracle-epm/links' +import { getOracleEpmRouteSpace } from '@/lib/internal/oracle-epm/route-space' +import type { + OracleEpmClientResponse, + OracleEpmDestination, + OracleEpmEndpoint, + OracleEpmPathPart, + OracleEpmQueryParameter, + OracleEpmRequestInput, + OracleEpmReturnedLinkPolicy, + OracleEpmValidatedLink, +} from '@/lib/internal/oracle-epm/types' + +const SAFE_TOKEN = /^[A-Za-z0-9+/]+={0,2}$/ +const validatedLinks = new WeakMap< + object, + { owner: object; url: string; policy: OracleEpmReturnedLinkPolicyDefinition } +>() + +function assertExactKeys( + input: Readonly> | undefined, + allowed: readonly string[] +): void { + for (const key of Object.keys(input ?? {})) { + if (!allowed.includes(key)) throw oracleEpmLocalError('invalid_input') + } + if (new Set(Object.keys(input ?? {})).size !== Object.keys(input ?? {}).length) { + throw oracleEpmLocalError('invalid_input') + } +} + +function validatePathValue( + value: unknown, + declaration: Extract +): string { + if ( + typeof value !== 'string' || + !value || + value === '.' || + value === '..' || + /[/\\\u0000-\u001f\u007f]/.test(value) || + Buffer.byteLength(value, 'utf8') > declaration.maxBytes || + (declaration.pattern && !declaration.pattern.test(value)) + ) { + throw oracleEpmLocalError('invalid_input') + } + return value +} + +function buildPath( + parts: readonly OracleEpmPathPart[], + values: Readonly> | undefined +): string[] { + const parameterNames = parts + .filter( + (part): part is Extract => part.kind === 'parameter' + ) + .map((part) => part.name) + assertExactKeys(values, parameterNames) + return parts.map((part) => { + if (part.kind === 'literal') return part.value + return validatePathValue(values?.[part.name], part) + }) +} + +function serializeQueryValue(value: unknown, declaration: OracleEpmQueryParameter): string { + if (declaration.kind === 'string') { + if ( + typeof value !== 'string' || + Buffer.byteLength(value, 'utf8') > declaration.maxBytes || + (declaration.pattern && !declaration.pattern.test(value)) + ) + throw oracleEpmLocalError('invalid_input') + return value + } + if (declaration.kind === 'integer') { + if ( + typeof value !== 'number' || + !Number.isSafeInteger(value) || + value < declaration.minimum || + value > declaration.maximum + ) + throw oracleEpmLocalError('invalid_input') + return String(value) + } + if (typeof value !== 'boolean') throw oracleEpmLocalError('invalid_input') + return String(value) +} + +function buildQuery( + declarations: Readonly>, + values: Readonly> | undefined +): URLSearchParams { + assertExactKeys(values, Object.keys(declarations)) + const query = new URLSearchParams() + for (const [name, declaration] of Object.entries(declarations)) { + const value = values?.[name] + if (value === undefined) { + if (declaration.required) throw oracleEpmLocalError('invalid_input') + continue + } + query.set(name, serializeQueryValue(value, declaration)) + } + return query +} + +function buildHeaders( + declarations: OracleEpmEndpointDefinition['headers'], + values: OracleEpmRequestInput['headers'], + accessToken: string, + bodyMode: OracleEpmEndpointDefinition['body'], + responseMode: OracleEpmEndpointDefinition['response'] +): Record { + const declared = declarations ?? {} + assertExactKeys(values, Object.keys(declared)) + const headers: Record = { + Authorization: `Basic ${accessToken}`, + Accept: responseMode === 'json' ? 'application/json' : '*/*', + } + for (const [inputName, declaration] of Object.entries(declared)) { + const value = values?.[inputName] + if (value === undefined) { + if (declaration.required) throw oracleEpmLocalError('invalid_input') + continue + } + if ( + /\r|\n|\u0000/.test(value) || + Buffer.byteLength(value, 'utf8') > declaration.maxBytes || + (declaration.pattern && !declaration.pattern.test(value)) + ) + throw oracleEpmLocalError('invalid_input') + headers[declaration.name] = value + } + const hasContentType = Object.keys(headers).some((name) => name.toLowerCase() === 'content-type') + if (bodyMode === 'json' && !hasContentType) headers['Content-Type'] = 'application/json' + if (bodyMode === 'stream' && !hasContentType) headers['Content-Type'] = 'application/octet-stream' + return headers +} + +function buildBody( + endpoint: OracleEpmEndpointDefinition, + input: OracleEpmRequestInput +): string | Uint8Array | undefined { + if (endpoint.body === 'none') { + if (input.json !== undefined || input.stream !== undefined) + throw oracleEpmLocalError('invalid_input') + return undefined + } + if (endpoint.body === 'json') { + if (input.json === undefined || input.stream !== undefined) + throw oracleEpmLocalError('invalid_input') + let body: string + try { + body = JSON.stringify(input.json) + } catch { + throw oracleEpmLocalError('invalid_input') + } + if (typeof body !== 'string') throw oracleEpmLocalError('invalid_input') + if (Buffer.byteLength(body, 'utf8') > (endpoint.maxRequestBytes ?? 0)) { + throw oracleEpmLocalError('payload_too_large') + } + return body + } + if (!(input.stream instanceof Uint8Array) || input.json !== undefined) + throw oracleEpmLocalError('invalid_input') + if (input.stream.byteLength > (endpoint.maxRequestBytes ?? 0)) { + throw oracleEpmLocalError('payload_too_large') + } + return input.stream +} + +function getCorrelationId( + response: SecureFetchResponse, + endpoint: OracleEpmEndpointDefinition +): string | undefined { + return endpoint.errors?.correlationHeaders + ?.map((name) => validateOracleEpmCorrelationId(response.headers.get(name))) + .find((value): value is string => value !== undefined) +} + +async function projectResponse( + response: SecureFetchResponse, + endpoint: OracleEpmEndpointDefinition +): Promise { + const correlationId = getCorrelationId(response, endpoint) + if (endpoint.response === 'empty') { + await response.body?.cancel().catch(() => undefined) + return Object.freeze({ status: response.status, correlationId }) + } + if (endpoint.response === 'stream') { + if (!response.body) throw oracleEpmLocalError('invalid_response') + const rawLength = response.headers.get('content-length') + const parsedLength = rawLength === null ? undefined : Number.parseInt(rawLength, 10) + return Object.freeze({ + status: response.status, + body: response.body, + ...(Number.isSafeInteger(parsedLength) && parsedLength !== undefined && parsedLength >= 0 + ? { contentLength: parsedLength } + : {}), + ...(response.headers.get('content-type') + ? { contentType: response.headers.get('content-type') ?? undefined } + : {}), + correlationId, + }) + } + try { + const data = await response.json() + return Object.freeze({ status: response.status, data, correlationId }) + } catch { + throw oracleEpmLocalError('invalid_response') + } +} + +function matchReturnedPath(candidate: string[], expected: readonly OracleEpmPathPart[]): void { + if (candidate.length !== expected.length) throw oracleEpmLocalError('invalid_input') + for (let index = 0; index < expected.length; index += 1) { + let decoded: string + try { + decoded = decodeURIComponent(candidate[index]) + } catch { + throw oracleEpmLocalError('invalid_input') + } + const part = expected[index] + if (part.kind === 'literal') { + if (decoded !== part.value) throw oracleEpmLocalError('invalid_input') + } else { + validatePathValue(decoded, part) + } + } +} + +/** Fixed-origin client that consumes only branded declarations and link capabilities. */ +export interface OracleEpmClient { + request( + endpoint: OracleEpmEndpoint, + input?: OracleEpmRequestInput + ): Promise + validateReturnedLink( + policy: OracleEpmReturnedLinkPolicy, + link: { rel: string; href: string; method?: string } + ): OracleEpmValidatedLink + requestValidatedLink( + link: OracleEpmValidatedLink, + signal?: AbortSignal + ): Promise +} + +/** Creates a fixed-destination Oracle EPM client from resolved credential material. */ +export function createOracleEpmClient(input: { + instanceUrl: string + accessToken: string +}): OracleEpmClient { + const destination: OracleEpmDestination = defineOracleEpmDestination(input.instanceUrl) + const destinationData = getOracleEpmDestination(destination) + if (!input.accessToken || input.accessToken.length > 4_096 || !SAFE_TOKEN.test(input.accessToken)) + throw oracleEpmLocalError('invalid_configuration') + const owner = Object.freeze({}) + + const perform = async ( + url: string, + endpoint: OracleEpmEndpointDefinition, + request: OracleEpmRequestInput = {} + ): Promise => { + const body = buildBody(endpoint, request) + const headers = buildHeaders( + endpoint.headers, + request.headers, + input.accessToken, + endpoint.body, + endpoint.response + ) + const deadlineSignal = AbortSignal.timeout(endpoint.timeoutMs) + const signal = request.signal + ? AbortSignal.any([request.signal, deadlineSignal]) + : deadlineSignal + const validation = await validateUrlWithDNS( + destinationData.origin, + 'Oracle EPM destination', + 'configuredEndpoint', + { logDetails: false } + ) + if (request.signal?.aborted) { + throw request.signal.reason ?? new DOMException('Aborted', 'AbortError') + } + if (deadlineSignal.aborted) throw oracleEpmLocalError('timeout', true) + if (!validation.isValid) throw oracleEpmLocalError('invalid_configuration') + const maxAttempts = endpoint.retry?.maxAttempts ?? 1 + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + let response: SecureFetchResponse + try { + response = await secureFetchWithPinnedIP(url, validation.resolvedIP, { + profile: 'configuredEndpoint', + method: endpoint.method, + headers, + body, + timeout: endpoint.timeoutMs, + maxRedirects: 0, + maxResponseBytes: endpoint.maxResponseBytes, + signal, + logUrlValidationDetails: false, + }) + } catch (error) { + if (isPayloadSizeLimitError(error)) throw oracleEpmLocalError('payload_too_large') + if (request.signal?.aborted) throw request.signal.reason ?? error + if (deadlineSignal.aborted) throw oracleEpmLocalError('timeout', true) + throw oracleEpmLocalError( + error instanceof Error && /timed out/i.test(error.message) + ? 'timeout' + : 'service_unavailable', + true + ) + } + if (response.ok) { + try { + return await projectResponse(response, endpoint) + } catch (error) { + if (request.signal?.aborted) { + throw request.signal.reason ?? error + } + if (deadlineSignal.aborted) throw oracleEpmLocalError('timeout', true) + throw error + } + } + const retryable = Boolean(endpoint.retry?.statuses.includes(response.status)) + if (!retryable || attempt === maxAttempts) + throw await oracleEpmErrorFromResponse(response, endpoint.errors, retryable) + await response.body?.cancel().catch(() => undefined) + const delay = backoffWithJitter(attempt, null, { + baseMs: endpoint.retry?.initialDelayMs, + maxMs: endpoint.retry?.maxDelayMs, + }) + try { + await interruptibleSleep(delay, signal) + } catch (error) { + if (request.signal?.aborted) throw request.signal.reason ?? error + if (deadlineSignal.aborted) throw oracleEpmLocalError('timeout', true) + throw error + } + } + throw oracleEpmLocalError('service_unavailable', true) + } + + const client: OracleEpmClient = Object.freeze({ + async request(endpointValue: OracleEpmEndpoint, request: OracleEpmRequestInput = {}) { + const endpoint = getOracleEpmEndpoint(endpointValue) + const route = getOracleEpmRouteSpace(endpoint.routeSpace) + const path = [ + ...destinationData.baseSegments, + ...route.context, + endpoint.version, + ...buildPath(endpoint.path, request.pathParams), + ] + const url = new URL(`${destinationData.origin}/${path.map(encodeURIComponent).join('/')}`) + const query = buildQuery(endpoint.query ?? {}, request.query) + url.search = query.toString() + return perform(url.toString(), endpoint, request) + }, + validateReturnedLink( + policyValue: OracleEpmReturnedLinkPolicy, + link: { rel: string; href: string; method?: string } + ) { + const policy = getOracleEpmReturnedLinkPolicy(policyValue) + if ( + link.rel !== policy.relation || + (link.method !== undefined && link.method !== policy.method) || + typeof link.href !== 'string' || + link.href.length > 8_192 + ) + throw oracleEpmLocalError('invalid_input') + let url: URL + try { + url = new URL(link.href) + } catch { + throw oracleEpmLocalError('invalid_input') + } + if (url.origin !== destinationData.origin || url.username || url.password || url.hash) + throw oracleEpmLocalError('invalid_input') + const route = getOracleEpmRouteSpace(policy.routeSpace) + const candidate = url.pathname.split('/').filter(Boolean) + const prefix = [...destinationData.baseSegments, ...route.context, policy.version] + const prefixMatches = (expected: readonly string[]): boolean => { + try { + return expected.every( + (part, index) => decodeURIComponent(candidate[index] ?? '') === part + ) + } catch { + return false + } + } + if (policy.preserveGatewayBasePath && !prefixMatches(prefix)) + throw oracleEpmLocalError('invalid_input') + const pathStart = policy.preserveGatewayBasePath ? prefix.length : route.context.length + 1 + if (!policy.preserveGatewayBasePath) { + const routePrefix = [...route.context, policy.version] + if (!prefixMatches(routePrefix)) throw oracleEpmLocalError('invalid_input') + } + matchReturnedPath(candidate.slice(pathStart), policy.path) + const seen = new Set() + for (const [name, value] of url.searchParams) { + if (seen.has(name) || !Object.hasOwn(policy.query, name)) + throw oracleEpmLocalError('invalid_input') + seen.add(name) + serializeQueryValue(value, policy.query[name]) + } + for (const [name, declaration] of Object.entries(policy.query)) { + if (declaration.required && !seen.has(name)) throw oracleEpmLocalError('invalid_input') + } + const handle = Object.freeze({}) as OracleEpmValidatedLink + validatedLinks.set(handle, { owner, url: url.toString(), policy }) + return handle + }, + async requestValidatedLink(handle: OracleEpmValidatedLink, signal?: AbortSignal) { + const value = validatedLinks.get(handle) + if (!value || value.owner !== owner) throw oracleEpmLocalError('invalid_input') + return perform(value.url, value.policy.endpoint, { signal }) + }, + }) + return client +} diff --git a/apps/sim/lib/internal/oracle-epm/destination.test.ts b/apps/sim/lib/internal/oracle-epm/destination.test.ts new file mode 100644 index 00000000000..a418628fc15 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/destination.test.ts @@ -0,0 +1,40 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { + defineOracleEpmDestination, + getOracleEpmDestination, + normalizeOracleEpmDestination, +} from '@/lib/internal/oracle-epm/destination' +import type { OracleEpmDestination } from '@/lib/internal/oracle-epm/types' + +describe('Oracle EPM destination', () => { + it('normalizes and preserves a credential-owned gateway base path', () => { + expect(normalizeOracleEpmDestination(' https://EPM.example.com/gateway/acme/ ')).toBe( + 'https://epm.example.com/gateway/acme' + ) + const destination = defineOracleEpmDestination('https://epm.example.com/gateway/acme') + expect(Object.isFrozen(destination)).toBe(true) + expect(getOracleEpmDestination(destination)).toMatchObject({ + origin: 'https://epm.example.com', + baseSegments: ['gateway', 'acme'], + }) + }) + + it.each([ + 'http://epm.example.com', + 'https://user@epm.example.com', + 'https://epm.example.com?token=secret', + 'https://epm.example.com/#fragment', + 'https://epm.example.com/%2e%2e/admin', + 'https://epm.example.com/a%2Fb', + 'https://epm.example.com/a\\b', + ])('rejects unsafe destination %j', (value) => { + expect(() => defineOracleEpmDestination(value)).toThrow() + }) + + it('rejects forged destination objects', () => { + expect(() => getOracleEpmDestination({} as OracleEpmDestination)).toThrow( + 'not a valid declaration' + ) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm/destination.ts b/apps/sim/lib/internal/oracle-epm/destination.ts new file mode 100644 index 00000000000..ccb51ec3e29 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/destination.ts @@ -0,0 +1,89 @@ +import type { OracleEpmDestination } from '@/lib/internal/oracle-epm/types' + +const MAX_DESTINATION_LENGTH = 2_048 +const MAX_PATH_SEGMENTS = 32 +const MAX_PATH_SEGMENT_BYTES = 255 +const FORBIDDEN_TEXT = /[\u0000-\u001f\u007f\\]/ +const destinations = new WeakMap() + +function decodeSegment(segment: string): string { + try { + return decodeURIComponent(segment) + } catch { + throw new Error('Oracle EPM environment URL has invalid path encoding') + } +} + +/** Validates and freezes the credential-bound Oracle EPM environment URL. */ +export function defineOracleEpmDestination(rawUrl: string): OracleEpmDestination { + const value = rawUrl.trim() + if ( + !value || + value.length > MAX_DESTINATION_LENGTH || + value.includes('%') || + FORBIDDEN_TEXT.test(value) + ) { + throw new Error('Oracle EPM environment URL is invalid') + } + + let parsed: URL + try { + parsed = new URL(value) + } catch { + throw new Error('Oracle EPM environment URL is invalid') + } + if ( + parsed.protocol !== 'https:' || + parsed.username || + parsed.password || + parsed.search || + parsed.hash + ) { + throw new Error( + 'Oracle EPM environment URL must be an HTTPS destination without credentials, query, or fragment' + ) + } + + const encodedSegments = parsed.pathname.split('/').filter(Boolean) + if (encodedSegments.length > MAX_PATH_SEGMENTS) { + throw new Error('Oracle EPM environment URL base path has too many segments') + } + const baseSegments = encodedSegments.map((encoded) => { + const decoded = decodeSegment(encoded) + if ( + !decoded || + decoded === '.' || + decoded === '..' || + decoded.includes('/') || + FORBIDDEN_TEXT.test(decoded) || + Buffer.byteLength(decoded, 'utf8') > MAX_PATH_SEGMENT_BYTES + ) { + throw new Error('Oracle EPM environment URL base path is invalid') + } + return decoded + }) + + const destination = Object.freeze({}) as OracleEpmDestination + destinations.set(destination, { + origin: parsed.origin, + baseSegments: Object.freeze(baseSegments), + }) + return destination +} + +/** Returns module-private destination data after rejecting forged values. */ +export function getOracleEpmDestination(destination: OracleEpmDestination): { + origin: string + baseSegments: readonly string[] + canonicalUrl: string +} { + const value = destinations.get(destination) + if (!value) throw new Error('Oracle EPM destination is not a valid declaration') + const suffix = value.baseSegments.map(encodeURIComponent).join('/') + return { ...value, canonicalUrl: suffix ? `${value.origin}/${suffix}` : value.origin } +} + +/** Canonical public value stored in the credential token result. */ +export function normalizeOracleEpmDestination(rawUrl: string): string { + return getOracleEpmDestination(defineOracleEpmDestination(rawUrl)).canonicalUrl +} diff --git a/apps/sim/lib/internal/oracle-epm/endpoint.test.ts b/apps/sim/lib/internal/oracle-epm/endpoint.test.ts new file mode 100644 index 00000000000..2fd50a4919c --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/endpoint.test.ts @@ -0,0 +1,93 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { + getOracleEpmEndpoint, + oracleEpmLiteral, + oracleEpmPathParameter, + oracleEpmQuery, +} from '@/lib/internal/oracle-epm/endpoint' +import { defineOracleEpmRouteSpace } from '@/lib/internal/oracle-epm/route-space' +import type { OracleEpmEndpoint } from '@/lib/internal/oracle-epm/types' + +const routes = defineOracleEpmRouteSpace({ + context: ['Synthetic', 'rest'], + allowedVersions: ['v3', 'V1'], +}) + +describe('Oracle EPM endpoints', () => { + it('freezes the complete static contract', () => { + const endpoint = routes.defineEndpoint({ + method: 'GET', + version: 'V1', + path: [oracleEpmLiteral('jobs'), oracleEpmPathParameter('jobId', { maxBytes: 64 })], + query: { limit: oracleEpmQuery.integer({ minimum: 1, maximum: 100 }) }, + headers: { etag: { name: 'If-None-Match', maxBytes: 128 } }, + body: 'none', + response: 'json', + timeoutMs: 5_000, + maxResponseBytes: 1_024, + }) + const declaration = getOracleEpmEndpoint(endpoint) + expect(declaration.version).toBe('V1') + expect(Object.isFrozen(endpoint)).toBe(true) + expect(Object.isFrozen(declaration.path)).toBe(true) + expect(Object.isFrozen(declaration.query)).toBe(true) + }) + + it('rejects versions with the wrong case and dangerous headers', () => { + expect(() => + routes.defineEndpoint({ + method: 'GET', + version: 'v1', + path: [], + body: 'none', + response: 'json', + timeoutMs: 1_000, + maxResponseBytes: 100, + }) + ).toThrow('version') + expect(() => + routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [], + headers: { raw: { name: 'Authorization', maxBytes: 10 } }, + body: 'none', + response: 'json', + timeoutMs: 1_000, + maxResponseBytes: 100, + }) + ).toThrow('header') + }) + + it('requires bounded request bodies and safe retry policies', () => { + expect(() => + routes.defineEndpoint({ + method: 'POST', + version: 'v3', + path: [], + body: 'json', + response: 'json', + timeoutMs: 1_000, + maxResponseBytes: 100, + }) + ).toThrow('request limit') + expect(() => + routes.defineEndpoint({ + method: 'POST', + version: 'v3', + path: [], + body: 'json', + maxRequestBytes: 100, + response: 'json', + timeoutMs: 1_000, + maxResponseBytes: 100, + retry: { maxAttempts: 2, statuses: [503], initialDelayMs: 1, maxDelayMs: 2 }, + }) + ).toThrow('retry policy') + }) + + it('rejects forged endpoints', () => { + expect(() => getOracleEpmEndpoint({} as OracleEpmEndpoint)).toThrow('not a valid declaration') + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm/endpoint.ts b/apps/sim/lib/internal/oracle-epm/endpoint.ts new file mode 100644 index 00000000000..8db7a5a1791 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/endpoint.ts @@ -0,0 +1,292 @@ +import { DEFAULT_MAX_RESPONSE_BYTES } from '@/lib/core/security/input-validation.server' +import { getOracleEpmRouteSpace } from '@/lib/internal/oracle-epm/route-space' +import type { + OracleEpmEndpoint, + OracleEpmEndpointDeclaration, + OracleEpmHeaderDeclaration, + OracleEpmPathPart, + OracleEpmQueryParameter, + OracleEpmRouteSpace, +} from '@/lib/internal/oracle-epm/types' + +const MAX_DECLARATION_ENTRIES = 32 +const MAX_LITERAL_BYTES = 255 +const FORBIDDEN_HEADERS = new Set([ + 'accept', + 'authorization', + 'connection', + 'content-length', + 'cookie', + 'host', + 'proxy-authorization', + 'set-cookie', + 'transfer-encoding', +]) +const TOKEN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/ +const endpoints = new WeakMap() + +/** Internal frozen endpoint metadata available only after runtime-brand validation. */ +export interface OracleEpmEndpointDefinition extends OracleEpmEndpointDeclaration { + readonly routeSpace: OracleEpmRouteSpace +} + +function validatePattern(pattern: RegExp | undefined, label: string): void { + if (pattern && (pattern.global || pattern.sticky)) { + throw new Error(`${label} pattern cannot use global or sticky flags`) + } +} + +function validatePath(path: readonly OracleEpmPathPart[]): void { + if (!Array.isArray(path) || path.length > MAX_DECLARATION_ENTRIES) { + throw new Error('Oracle EPM endpoint path is invalid') + } + const names = new Set() + for (const part of path) { + if (part.kind === 'literal') { + if ( + !part.value || + part.value === '.' || + part.value === '..' || + /[/\\\u0000-\u001f\u007f]/.test(part.value) || + Buffer.byteLength(part.value, 'utf8') > MAX_LITERAL_BYTES + ) { + throw new Error('Oracle EPM endpoint literal path segment is invalid') + } + continue + } + if ( + part.kind !== 'parameter' || + !TOKEN.test(part.name) || + names.has(part.name) || + !Number.isInteger(part.maxBytes) || + part.maxBytes < 1 || + part.maxBytes > MAX_LITERAL_BYTES + ) { + throw new Error('Oracle EPM endpoint path parameter declaration is invalid') + } + validatePattern(part.pattern, `Oracle EPM path parameter ${part.name}`) + names.add(part.name) + } +} + +function validateQuery(query: Readonly> = {}): void { + const entries = Object.entries(query) + if (entries.length > MAX_DECLARATION_ENTRIES) + throw new Error('Too many Oracle EPM query parameters') + for (const [name, declaration] of entries) { + if (!TOKEN.test(name)) throw new Error('Oracle EPM query parameter name is invalid') + if (declaration.kind === 'string') { + if ( + !Number.isInteger(declaration.maxBytes) || + declaration.maxBytes < 1 || + declaration.maxBytes > 4_096 + ) { + throw new Error(`Oracle EPM query parameter ${name} has an invalid limit`) + } + validatePattern(declaration.pattern, `Oracle EPM query parameter ${name}`) + } else if (declaration.kind === 'integer') { + if ( + !Number.isSafeInteger(declaration.minimum) || + !Number.isSafeInteger(declaration.maximum) || + declaration.minimum > declaration.maximum + ) { + throw new Error(`Oracle EPM query parameter ${name} has invalid integer bounds`) + } + } else if (declaration.kind !== 'boolean') { + throw new Error(`Oracle EPM query parameter ${name} has an invalid type`) + } + } +} + +function validateHeaders(headers: Readonly> = {}): void { + const entries = Object.entries(headers) + if (entries.length > MAX_DECLARATION_ENTRIES) throw new Error('Too many Oracle EPM headers') + const wireNames = new Set() + for (const [inputName, declaration] of entries) { + const wireName = declaration.name.toLowerCase() + if ( + !TOKEN.test(inputName) || + !/^[A-Za-z0-9-]+$/.test(declaration.name) || + wireNames.has(wireName) || + FORBIDDEN_HEADERS.has(wireName) + ) { + throw new Error('Oracle EPM header declaration is invalid') + } + if ( + !Number.isInteger(declaration.maxBytes) || + declaration.maxBytes < 1 || + declaration.maxBytes > 8_192 + ) { + throw new Error(`Oracle EPM header ${inputName} has an invalid limit`) + } + validatePattern(declaration.pattern, `Oracle EPM header ${inputName}`) + wireNames.add(wireName) + } +} + +function clonePattern(pattern: RegExp | undefined): RegExp | undefined { + return pattern ? new RegExp(pattern.source, pattern.flags) : undefined +} + +function freezeDeclaration( + declaration: OracleEpmEndpointDeclaration +): OracleEpmEndpointDeclaration { + const path = declaration.path.map((part) => + Object.freeze( + part.kind === 'parameter' ? { ...part, pattern: clonePattern(part.pattern) } : { ...part } + ) + ) + const query = Object.fromEntries( + Object.entries(declaration.query ?? {}).map(([name, value]) => [ + name, + Object.freeze( + value.kind === 'string' ? { ...value, pattern: clonePattern(value.pattern) } : { ...value } + ), + ]) + ) + const headers = Object.fromEntries( + Object.entries(declaration.headers ?? {}).map(([name, value]) => [ + name, + Object.freeze({ ...value, pattern: clonePattern(value.pattern) }), + ]) + ) + const errors = declaration.errors + ? Object.freeze({ + ...declaration.errors, + providerCodePath: Object.freeze([...(declaration.errors.providerCodePath ?? [])]), + allowedProviderCodes: Object.freeze([...(declaration.errors.allowedProviderCodes ?? [])]), + correlationHeaders: Object.freeze([...(declaration.errors.correlationHeaders ?? [])]), + }) + : undefined + const retry = declaration.retry + ? Object.freeze({ + ...declaration.retry, + statuses: Object.freeze([...declaration.retry.statuses]), + }) + : undefined + return Object.freeze({ + ...declaration, + path: Object.freeze(path), + query: Object.freeze(query), + headers: Object.freeze(headers), + errors, + retry, + }) +} + +/** Defines a literal path segment that can never be replaced by tool input. */ +export function oracleEpmLiteral(value: string): OracleEpmPathPart { + return Object.freeze({ kind: 'literal', value }) +} + +/** Defines one individually encoded path parameter. */ +export function oracleEpmPathParameter( + name: string, + options: { maxBytes: number; pattern?: RegExp } +): OracleEpmPathPart { + return Object.freeze({ kind: 'parameter', name, ...options }) +} + +/** Creates a branded endpoint after validating its complete static declaration. */ +export function defineOracleEpmEndpoint( + routeSpace: OracleEpmRouteSpace, + declaration: OracleEpmEndpointDeclaration +): OracleEpmEndpoint { + const route = getOracleEpmRouteSpace(routeSpace) + if (!route.allowedVersions.includes(declaration.version)) + throw new Error('Oracle EPM endpoint version is not declared by its route space') + if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].includes(declaration.method)) + throw new Error('Oracle EPM endpoint method is invalid') + validatePath(declaration.path) + validateQuery(declaration.query) + validateHeaders(declaration.headers) + if ( + !['none', 'json', 'stream'].includes(declaration.body) || + !['empty', 'json', 'stream'].includes(declaration.response) + ) { + throw new Error('Oracle EPM endpoint body or response mode is invalid') + } + if ( + !Number.isInteger(declaration.timeoutMs) || + declaration.timeoutMs < 100 || + declaration.timeoutMs > 300_000 + ) + throw new Error('Oracle EPM endpoint timeout is invalid') + if ( + !Number.isInteger(declaration.maxResponseBytes) || + declaration.maxResponseBytes < 1 || + declaration.maxResponseBytes > DEFAULT_MAX_RESPONSE_BYTES + ) { + throw new Error('Oracle EPM endpoint response limit is invalid') + } + if ( + (declaration.body === 'none' && declaration.maxRequestBytes !== undefined) || + (declaration.body !== 'none' && + (!Number.isInteger(declaration.maxRequestBytes) || + (declaration.maxRequestBytes ?? 0) < 1 || + (declaration.maxRequestBytes ?? 0) > DEFAULT_MAX_RESPONSE_BYTES)) + ) { + throw new Error('Oracle EPM endpoint request limit is invalid') + } + if (declaration.retry) { + const { maxAttempts, statuses, initialDelayMs, maxDelayMs } = declaration.retry + if ( + !['GET', 'HEAD', 'PUT', 'DELETE'].includes(declaration.method) || + !Number.isInteger(maxAttempts) || + maxAttempts < 1 || + maxAttempts > 5 || + !statuses.length || + statuses.some((status) => !Number.isInteger(status) || status < 400 || status > 599) || + !Number.isInteger(initialDelayMs) || + !Number.isInteger(maxDelayMs) || + initialDelayMs < 0 || + maxDelayMs < initialDelayMs || + maxDelayMs > 30_000 + ) { + throw new Error('Oracle EPM endpoint retry policy is invalid') + } + } + const allowedProviderCodes = declaration.errors?.allowedProviderCodes ?? [] + const correlationHeaders = declaration.errors?.correlationHeaders ?? [] + if ( + declaration.errors?.providerCodePath?.some((part) => !TOKEN.test(part)) || + allowedProviderCodes.some((code) => !code || code.length > 128) || + new Set(allowedProviderCodes).size !== allowedProviderCodes.length || + correlationHeaders.some((name) => !/^[A-Za-z0-9-]+$/.test(name)) || + new Set(correlationHeaders.map((name) => name.toLowerCase())).size !== correlationHeaders.length + ) { + throw new Error('Oracle EPM endpoint error policy is invalid') + } + + const endpoint = Object.freeze({}) as OracleEpmEndpoint + endpoints.set(endpoint, Object.freeze({ ...freezeDeclaration(declaration), routeSpace })) + return endpoint +} + +/** Reads an endpoint declaration only after its runtime brand is verified. */ +export function getOracleEpmEndpoint(endpoint: OracleEpmEndpoint): OracleEpmEndpointDefinition { + const definition = endpoints.get(endpoint) + if (!definition) throw new Error('Oracle EPM endpoint is not a valid declaration') + return definition +} + +/** Factories for bounded scalar query declarations. */ +export const oracleEpmQuery = Object.freeze({ + string(options: { + required?: boolean + maxBytes: number + pattern?: RegExp + }): OracleEpmQueryParameter { + return Object.freeze({ kind: 'string', ...options }) + }, + integer(options: { + required?: boolean + minimum: number + maximum: number + }): OracleEpmQueryParameter { + return Object.freeze({ kind: 'integer', ...options }) + }, + boolean(options: { required?: boolean } = {}): OracleEpmQueryParameter { + return Object.freeze({ kind: 'boolean', ...options }) + }, +}) diff --git a/apps/sim/lib/internal/oracle-epm/errors.test.ts b/apps/sim/lib/internal/oracle-epm/errors.test.ts new file mode 100644 index 00000000000..f311dcf0151 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/errors.test.ts @@ -0,0 +1,70 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import type { SecureFetchResponse } from '@/lib/core/security/input-validation.server' +import { + oracleEpmErrorFromResponse, + validateOracleEpmCorrelationId, +} from '@/lib/internal/oracle-epm/errors' + +function response(body: unknown, correlationId = 'req-123'): SecureFetchResponse { + const encoded = new TextEncoder().encode(JSON.stringify(body)) + return { + ok: false, + status: 400, + statusText: 'Bad Request', + headers: { + get: (name: string) => (name === 'x-request-id' ? correlationId : null), + } as SecureFetchResponse['headers'], + body: new ReadableStream({ + start(controller) { + controller.enqueue(encoded) + controller.close() + }, + }), + text: async () => JSON.stringify(body), + json: async () => body, + arrayBuffer: async () => new ArrayBuffer(0), + } +} + +describe('Oracle EPM public errors', () => { + it('exposes only allowlisted codes and validated correlation ids', async () => { + const error = await oracleEpmErrorFromResponse( + response({ + code: 'SAFE_CODE', + message: 'password=super-secret', + detail: 'secret', + }), + { + providerCodePath: ['code'], + allowedProviderCodes: ['SAFE_CODE'], + correlationHeaders: ['x-request-id'], + }, + false + ) + expect(error).toMatchObject({ + category: 'invalid_input', + status: 400, + providerCode: 'SAFE_CODE', + correlationId: 'req-123', + retryable: false, + }) + expect(JSON.stringify(error)).not.toContain('super-secret') + expect(error.message).not.toContain('html') + }) + + it('drops arbitrary codes and malformed correlation ids', async () => { + const error = await oracleEpmErrorFromResponse( + response({ code: 'UNREVIEWED' }, 'token secret'), + { + providerCodePath: ['code'], + allowedProviderCodes: ['SAFE_CODE'], + correlationHeaders: ['x-request-id'], + }, + false + ) + expect(error.providerCode).toBeUndefined() + expect(error.correlationId).toBeUndefined() + expect(validateOracleEpmCorrelationId('x'.repeat(129))).toBeUndefined() + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm/errors.ts b/apps/sim/lib/internal/oracle-epm/errors.ts new file mode 100644 index 00000000000..825b6d11696 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/errors.ts @@ -0,0 +1,149 @@ +import type { SecureFetchResponse } from '@/lib/core/security/input-validation.server' +import { + DEFAULT_MAX_ERROR_BODY_BYTES, + readResponseJsonWithLimit, +} from '@/lib/core/utils/stream-limits' +import type { OracleEpmErrorPolicyDeclaration } from '@/lib/internal/oracle-epm/types' + +/** Stable public failure categories emitted by the guarded transport. */ +export type OracleEpmErrorCategory = + | 'authentication_required' + | 'conflict' + | 'forbidden' + | 'invalid_configuration' + | 'invalid_input' + | 'invalid_response' + | 'not_found' + | 'payload_too_large' + | 'rate_limited' + | 'service_unavailable' + | 'timeout' + +const PUBLIC_MESSAGES: Readonly> = Object.freeze({ + authentication_required: 'Oracle EPM authentication failed', + conflict: 'Oracle EPM rejected the request because of a conflict', + forbidden: 'Oracle EPM denied the request', + invalid_configuration: 'Oracle EPM is not configured correctly', + invalid_input: 'Oracle EPM rejected the request input', + invalid_response: 'Oracle EPM returned an invalid response', + not_found: 'The requested Oracle EPM resource was not found', + payload_too_large: 'The Oracle EPM payload exceeded the allowed size', + rate_limited: 'Oracle EPM rate limited the request', + service_unavailable: 'Oracle EPM is temporarily unavailable', + timeout: 'The Oracle EPM request timed out', +}) + +const CORRELATION_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/ +const ERROR_CONSTRUCTION_TOKEN = Symbol('oracle-epm-error') + +/** Public transport error whose fields are intentionally finite and non-provider-authored. */ +export class OracleEpmError extends Error { + readonly category: OracleEpmErrorCategory + readonly status?: number + readonly providerCode?: string + readonly retryable: boolean + readonly correlationId?: string + + constructor( + input: { + category: OracleEpmErrorCategory + status?: number + providerCode?: string + retryable: boolean + correlationId?: string + }, + token: symbol + ) { + if (token !== ERROR_CONSTRUCTION_TOKEN) { + throw new Error('Oracle EPM errors can only be created by the guarded transport') + } + super(PUBLIC_MESSAGES[input.category]) + this.name = 'OracleEpmError' + this.category = input.category + this.status = input.status + this.providerCode = input.providerCode + this.retryable = input.retryable + this.correlationId = input.correlationId + Object.freeze(this) + } +} + +/** Admits only bounded, printable request identifiers. */ +export function validateOracleEpmCorrelationId(value: string | null): string | undefined { + return value && CORRELATION_ID.test(value) ? value : undefined +} + +/** Maps a provider HTTP failure into the finite public category set. */ +export function categoryForOracleEpmStatus(status: number): OracleEpmErrorCategory { + if (status === 400 || status === 422) return 'invalid_input' + if (status === 401) return 'authentication_required' + if (status === 403) return 'forbidden' + if (status === 404) return 'not_found' + if (status === 409 || status === 412) return 'conflict' + if (status === 413) return 'payload_too_large' + if (status === 429) return 'rate_limited' + if (status === 408 || status === 504) return 'timeout' + return 'service_unavailable' +} + +function readPath(value: unknown, path: readonly string[]): unknown { + let current = value + for (const part of path) { + if (typeof current !== 'object' || current === null || Array.isArray(current)) return undefined + current = (current as Record)[part] + } + return current +} + +/** + * Builds a safe error from a failed response. Provider text is read only to + * select an explicitly allowlisted code and is never retained or reflected. + */ +export async function oracleEpmErrorFromResponse( + response: SecureFetchResponse, + policy: OracleEpmErrorPolicyDeclaration | undefined, + retryable: boolean +): Promise { + let providerCode: string | undefined + if (policy?.providerCodePath?.length && policy.allowedProviderCodes?.length) { + try { + const body = await readResponseJsonWithLimit(response, { + maxBytes: DEFAULT_MAX_ERROR_BODY_BYTES, + label: 'Oracle EPM error response', + }) + const candidate = readPath(body, policy.providerCodePath) + if (typeof candidate === 'string' && policy.allowedProviderCodes.includes(candidate)) { + providerCode = candidate + } + } catch { + // Provider bodies are deliberately discarded, including parse failures. + } + } else { + await response.body?.cancel().catch(() => undefined) + } + const correlationId = policy?.correlationHeaders + ?.map((name) => validateOracleEpmCorrelationId(response.headers.get(name))) + .find((value): value is string => value !== undefined) + const hasPublicHttpStatus = + Number.isInteger(response.status) && response.status >= 400 && response.status <= 599 + return new OracleEpmError( + { + category: hasPublicHttpStatus + ? categoryForOracleEpmStatus(response.status) + : 'invalid_response', + status: hasPublicHttpStatus ? response.status : undefined, + providerCode, + retryable, + correlationId, + }, + ERROR_CONSTRUCTION_TOKEN + ) +} + +/** Creates a fixed-message failure for a local transport guard. */ +export function oracleEpmLocalError( + category: OracleEpmErrorCategory, + retryable = false +): OracleEpmError { + return new OracleEpmError({ category, retryable }, ERROR_CONSTRUCTION_TOKEN) +} diff --git a/apps/sim/lib/internal/oracle-epm/files.server.test.ts b/apps/sim/lib/internal/oracle-epm/files.server.test.ts new file mode 100644 index 00000000000..ffd4cef5acd --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/files.server.test.ts @@ -0,0 +1,258 @@ +/** @vitest-environment node */ +import { Readable } from 'node:stream' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + MAX_WORKSPACE_FILE_SIZE, + MAX_WORKSPACE_FORMDATA_FILE_SIZE, +} from '@/lib/uploads/shared/types' + +const mocks = vi.hoisted(() => ({ + abort: vi.fn(), + complete: vi.fn(), + createMultipartUpload: vi.fn(), + deleteFile: vi.fn(), + downloadFileStream: vi.fn(), + generateFileId: vi.fn(), + generatePresignedDownloadUrl: vi.fn(), + generateUniqueExecutionFileKey: vi.fn(), + verifyFileAccess: vi.fn(), + write: vi.fn(), +})) + +vi.mock('@/app/api/files/authorization', () => ({ verifyFileAccess: mocks.verifyFileAccess })) +vi.mock('@/lib/uploads/core/storage-service', () => ({ + createMultipartUpload: mocks.createMultipartUpload, + deleteFile: mocks.deleteFile, + downloadFileStream: mocks.downloadFileStream, + generatePresignedDownloadUrl: mocks.generatePresignedDownloadUrl, +})) +vi.mock('@/lib/uploads/contexts/execution/utils', () => ({ + generateFileId: mocks.generateFileId, + generateUniqueExecutionFileKey: mocks.generateUniqueExecutionFileKey, +})) + +import { + openOracleEpmSourceFile, + storeOracleEpmDownload, +} from '@/lib/internal/oracle-epm/files.server' + +const context = { + workspaceId: '00000000-0000-4000-8000-000000000001', + workflowId: '00000000-0000-4000-8000-000000000002', + executionId: '00000000-0000-4000-8000-000000000003', +} + +describe('Oracle EPM file primitives', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.verifyFileAccess.mockResolvedValue(true) + mocks.downloadFileStream.mockResolvedValue(Readable.from([Buffer.from('abc')])) + mocks.createMultipartUpload.mockResolvedValue({ + write: mocks.write, + complete: mocks.complete, + abort: mocks.abort, + }) + mocks.write.mockResolvedValue(undefined) + mocks.complete.mockResolvedValue({ key: 'execution/key/report.csv', size: 3 }) + mocks.abort.mockResolvedValue(undefined) + mocks.deleteFile.mockResolvedValue(undefined) + mocks.generateUniqueExecutionFileKey.mockReturnValue('execution/key/report.csv') + mocks.generateFileId.mockReturnValue('file-1') + mocks.generatePresignedDownloadUrl.mockResolvedValue('https://storage.example/signed') + }) + + it('authorizes before opening and counts source bytes while streaming', async () => { + const source = await openOracleEpmSourceFile({ + file: { + id: 'f', + name: 'report final.csv', + url: '', + size: 3, + type: 'text/csv', + key: 'workspace/key', + context: 'workspace', + }, + userId: 'user-1', + maxBytes: 3, + }) + const chunks: Buffer[] = [] + for await (const chunk of source.chunks) chunks.push(chunk) + expect(mocks.verifyFileAccess.mock.invocationCallOrder[0]).toBeLessThan( + mocks.downloadFileStream.mock.invocationCallOrder[0] + ) + expect(Buffer.concat(chunks).toString()).toBe('abc') + expect(source.fileName).toBe('report-final.csv') + }) + + it('rejects denied and over-limit source files before reading storage', async () => { + await expect( + openOracleEpmSourceFile({ + file: { + id: 'f', + name: 'x', + url: '', + size: 4, + type: '', + key: 'workspace/key', + context: 'workspace', + }, + userId: 'user-1', + maxBytes: 3, + }) + ).rejects.toThrow('maximum size') + expect(mocks.verifyFileAccess).not.toHaveBeenCalled() + + mocks.verifyFileAccess.mockResolvedValue(false) + await expect( + openOracleEpmSourceFile({ + file: { + id: 'f', + name: 'x', + url: '', + size: 1, + type: '', + key: 'workspace/key', + context: 'workspace', + }, + userId: 'user-1', + maxBytes: 3, + }) + ).rejects.toThrow('not found') + expect(mocks.downloadFileStream).not.toHaveBeenCalled() + }) + + it('clamps caller limits to existing workspace and execution-attachment limits', async () => { + const source = await openOracleEpmSourceFile({ + file: { + id: 'f', + name: 'x', + url: '', + size: 0, + type: '', + key: 'workspace/key', + context: 'workspace', + }, + userId: 'user-1', + maxBytes: MAX_WORKSPACE_FILE_SIZE + 1, + }) + expect(source.maxBytes).toBe(MAX_WORKSPACE_FILE_SIZE) + + await expect( + storeOracleEpmDownload({ + body: new ReadableStream(), + fileName: 'x', + context, + maxBytes: MAX_WORKSPACE_FORMDATA_FILE_SIZE + 10, + contentLength: MAX_WORKSPACE_FORMDATA_FILE_SIZE + 1, + }) + ).rejects.toThrow('maximum size') + expect(mocks.createMultipartUpload).not.toHaveBeenCalled() + }) + + it('rejects an untrusted execution storage context before creating a key', async () => { + await expect( + storeOracleEpmDownload({ + body: new ReadableStream(), + fileName: 'x', + context: { ...context, executionId: '../other' }, + maxBytes: 3, + }) + ).rejects.toThrow('context is invalid') + expect(mocks.generateUniqueExecutionFileKey).not.toHaveBeenCalled() + expect(mocks.createMultipartUpload).not.toHaveBeenCalled() + }) + + it('destroys an over-limit source stream during in-flight counting', async () => { + const stream = Readable.from([Buffer.from('abcd')]) + const destroy = vi.spyOn(stream, 'destroy') + mocks.downloadFileStream.mockResolvedValue(stream) + const source = await openOracleEpmSourceFile({ + file: { + id: 'f', + name: 'x', + url: '', + size: 0, + type: '', + key: 'workspace/key', + context: 'workspace', + }, + userId: 'user-1', + maxBytes: 3, + }) + await expect(async () => { + for await (const _chunk of source.chunks) { + // Consume the guarded stream. + } + }).rejects.toThrow('maximum size') + expect(destroy).toHaveBeenCalled() + }) + + it('streams a bounded provider response into execution storage and returns UserFile', async () => { + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1, 2, 3])) + controller.close() + }, + }) + await expect( + storeOracleEpmDownload({ + body, + fileName: '../report?.csv', + contentType: 'text/csv', + context, + maxBytes: MAX_WORKSPACE_FORMDATA_FILE_SIZE + 1, + }) + ).resolves.toEqual({ + id: 'file-1', + name: '.._report_.csv', + url: 'https://storage.example/signed', + size: 3, + type: 'text/csv', + key: 'execution/key/report.csv', + context: 'execution', + }) + expect(mocks.createMultipartUpload).toHaveBeenCalledWith( + expect.objectContaining({ + context: 'execution', + completionPolicy: 'create-only', + }) + ) + expect(mocks.write).toHaveBeenCalledWith(Buffer.from([1, 2, 3])) + expect(mocks.generatePresignedDownloadUrl).toHaveBeenCalledWith( + 'execution/key/report.csv', + 'execution', + 300 + ) + }) + + it('aborts partial storage on stream or size failure', async () => { + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1, 2, 3, 4])) + controller.close() + }, + }) + await expect( + storeOracleEpmDownload({ body, fileName: 'x', context, maxBytes: 3 }) + ).rejects.toThrow('maximum size') + expect(mocks.abort).toHaveBeenCalled() + expect(mocks.complete).not.toHaveBeenCalled() + }) + + it('removes a completed object if link generation fails', async () => { + mocks.complete.mockResolvedValue({ key: 'execution/key/report.csv', size: 0 }) + mocks.generatePresignedDownloadUrl.mockRejectedValue(new Error('presign failed')) + const body = new ReadableStream({ + start(controller) { + controller.close() + }, + }) + await expect( + storeOracleEpmDownload({ body, fileName: 'x', context, maxBytes: 3 }) + ).rejects.toThrow('presign failed') + expect(mocks.deleteFile).toHaveBeenCalledWith({ + key: 'execution/key/report.csv', + context: 'execution', + }) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm/files.server.ts b/apps/sim/lib/internal/oracle-epm/files.server.ts new file mode 100644 index 00000000000..f0d90b2b1c4 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/files.server.ts @@ -0,0 +1,194 @@ +import { assertKnownSizeWithinLimit, PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { + type ExecutionContext, + generateFileId, + generateUniqueExecutionFileKey, +} from '@/lib/uploads/contexts/execution/utils' +import { + createMultipartUpload, + deleteFile, + downloadFileStream, + generatePresignedDownloadUrl, +} from '@/lib/uploads/core/storage-service' +import { + MAX_WORKSPACE_FILE_SIZE, + MAX_WORKSPACE_FORMDATA_FILE_SIZE, +} from '@/lib/uploads/shared/types' +import { resolveTrustedFileContext } from '@/lib/uploads/utils/file-utils' +import { verifyFileAccess } from '@/app/api/files/authorization' +import { isUuid, sanitizeFileName } from '@/executor/constants' +import type { UserFile } from '@/executor/types' + +const EXECUTION_DOWNLOAD_URL_TTL_SECONDS = 300 + +function clampLimit(callerLimit: number, platformLimit: number): number { + if (!Number.isSafeInteger(callerLimit) || callerLimit < 1) { + throw new Error('Oracle EPM file limit must be a positive safe integer') + } + return Math.min(callerLimit, platformLimit) +} + +function safeFileName(fileName: string): string { + const sanitized = sanitizeFileName(fileName).slice(0, 255) + return !sanitized || sanitized === '.' || sanitized === '..' ? 'download' : sanitized +} + +function safeContentType(contentType: string | undefined): string { + return contentType && + /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/.test(contentType) + ? contentType + : 'application/octet-stream' +} + +function assertExecutionContext(context: ExecutionContext): void { + if (!isUuid(context.workspaceId) || !isUuid(context.workflowId) || !isUuid(context.executionId)) { + throw new Error('Oracle EPM execution file context is invalid') + } +} + +/** Authorized, byte-counted Sim source consumed by a product-owned upload protocol. */ +export interface OracleEpmSourceFile { + readonly fileName: string + readonly contentType: string + readonly maxBytes: number + readonly chunks: AsyncIterable +} + +/** Authorizes a Sim file, then opens a byte-counted source stream for a child uploader. */ +export async function openOracleEpmSourceFile(input: { + file: UserFile + userId: string + maxBytes: number + signal?: AbortSignal +}): Promise { + const { file, userId, signal } = input + if (!file.key || !userId) throw new Error('Oracle EPM source file is invalid') + const maxBytes = clampLimit(input.maxBytes, MAX_WORKSPACE_FILE_SIZE) + if (!Number.isSafeInteger(file.size) || file.size < 0) { + throw new Error('Oracle EPM source file metadata is invalid') + } + assertKnownSizeWithinLimit(file.size, maxBytes, 'Oracle EPM source file') + const context = resolveTrustedFileContext(file.key, file.context) + const allowed = await verifyFileAccess(file.key, userId, undefined, context, false) + if (!allowed) throw new Error('Oracle EPM source file was not found') + signal?.throwIfAborted() + + const chunks = (async function* boundedChunks(): AsyncIterable { + const stream = await downloadFileStream({ key: file.key, context }) + let bytes = 0 + const abort = () => stream.destroy(signal?.reason) + signal?.addEventListener('abort', abort, { once: true }) + try { + for await (const chunk of stream) { + signal?.throwIfAborted() + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array) + bytes += buffer.length + if (bytes > maxBytes) { + throw new PayloadSizeLimitError({ + label: 'Oracle EPM source file', + maxBytes, + observedBytes: bytes, + }) + } + yield buffer + } + } finally { + signal?.removeEventListener('abort', abort) + stream.destroy() + } + })() + + return Object.freeze({ + fileName: safeFileName(file.name), + contentType: safeContentType(file.type), + maxBytes, + chunks, + }) +} + +/** Streams a bounded Oracle response into existing execution-file storage. */ +export async function storeOracleEpmDownload(input: { + body: ReadableStream + fileName: string + contentType?: string + contentLength?: number + context: ExecutionContext + maxBytes: number + signal?: AbortSignal +}): Promise { + const maxBytes = clampLimit(input.maxBytes, MAX_WORKSPACE_FORMDATA_FILE_SIZE) + assertExecutionContext(input.context) + if (input.contentLength !== undefined) { + if (!Number.isSafeInteger(input.contentLength) || input.contentLength < 0) { + throw new Error('Oracle EPM download metadata is invalid') + } + assertKnownSizeWithinLimit(input.contentLength, maxBytes, 'Oracle EPM download') + } + const fileName = safeFileName(input.fileName) + const contentType = safeContentType(input.contentType) + const key = generateUniqueExecutionFileKey(input.context, fileName) + const upload = await createMultipartUpload({ + key, + context: 'execution', + contentType, + completionPolicy: 'create-only', + }) + const reader = input.body.getReader() + let bytes = 0 + let completed = false + let cleanupPromise: Promise | undefined + const cleanup = (): Promise => { + cleanupPromise ??= completed + ? deleteFile({ key, context: 'execution' }).catch(() => undefined) + : upload.abort().catch(() => undefined) + return cleanupPromise + } + const abort = () => { + void reader.cancel(input.signal?.reason).catch(() => undefined) + void cleanup() + } + input.signal?.addEventListener('abort', abort, { once: true }) + try { + while (true) { + input.signal?.throwIfAborted() + const { done, value } = await reader.read() + if (done) break + bytes += value.byteLength + if (bytes > maxBytes) { + throw new PayloadSizeLimitError({ + label: 'Oracle EPM download', + maxBytes, + observedBytes: bytes, + }) + } + await upload.write(Buffer.from(value)) + } + const stored = await upload.complete() + completed = true + if (stored.size !== bytes) { + throw new Error('Oracle EPM download storage size did not match the streamed bytes') + } + const url = await generatePresignedDownloadUrl( + stored.key, + 'execution', + EXECUTION_DOWNLOAD_URL_TTL_SECONDS + ) + return { + id: generateFileId(), + name: fileName, + url, + size: stored.size, + type: contentType, + key: stored.key, + context: 'execution', + } + } catch (error) { + await reader.cancel().catch(() => undefined) + await cleanup() + throw error + } finally { + input.signal?.removeEventListener('abort', abort) + if (!completed) await cleanup() + reader.releaseLock() + } +} diff --git a/apps/sim/lib/internal/oracle-epm/index.ts b/apps/sim/lib/internal/oracle-epm/index.ts new file mode 100644 index 00000000000..b3de5c86c9f --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/index.ts @@ -0,0 +1,37 @@ +export { + createOracleEpmClient, + type OracleEpmClient, +} from '@/lib/internal/oracle-epm/client.server' +export { + defineOracleEpmDestination, + normalizeOracleEpmDestination, +} from '@/lib/internal/oracle-epm/destination' +export { + oracleEpmLiteral, + oracleEpmPathParameter, + oracleEpmQuery, +} from '@/lib/internal/oracle-epm/endpoint' +export { OracleEpmError } from '@/lib/internal/oracle-epm/errors' +export { + type OracleEpmSourceFile, + openOracleEpmSourceFile, + storeOracleEpmDownload, +} from '@/lib/internal/oracle-epm/files.server' +export { + type OracleEpmPollClassification, + type OracleEpmPollOptions, + type OracleEpmPollResult, + pollOracleEpmJob, +} from '@/lib/internal/oracle-epm/jobs' +export { defineOracleEpmRouteSpace } from '@/lib/internal/oracle-epm/route-space' +export type { + OracleEpmClientResponse, + OracleEpmDestination, + OracleEpmEndpoint, + OracleEpmEndpointDeclaration, + OracleEpmRequestInput, + OracleEpmReturnedLinkPolicy, + OracleEpmReturnedLinkPolicyDeclaration, + OracleEpmRouteSpace, + OracleEpmValidatedLink, +} from '@/lib/internal/oracle-epm/types' diff --git a/apps/sim/lib/internal/oracle-epm/jobs.test.ts b/apps/sim/lib/internal/oracle-epm/jobs.test.ts new file mode 100644 index 00000000000..8e8aae188f4 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/jobs.test.ts @@ -0,0 +1,94 @@ +/** @vitest-environment node */ +import { describe, expect, it, vi } from 'vitest' +import { pollOracleEpmJob } from '@/lib/internal/oracle-epm/jobs' + +describe('pollOracleEpmJob', () => { + it('leaves status interpretation and result extraction to the child', async () => { + const read = vi + .fn() + .mockResolvedValueOnce({ status: 'RUNNING' }) + .mockResolvedValueOnce({ status: 'DONE', value: 42 }) + const result = await pollOracleEpmJob({ + read, + classify: (snapshot) => + snapshot.status === 'DONE' + ? { state: 'success' as const, result: snapshot.value } + : { state: 'pending' as const }, + maxWaitMs: 1_000, + cleanupReserveMs: 10, + maxAttempts: 3, + initialDelayMs: 1, + maxDelayMs: 1, + }) + expect(result).toEqual({ state: 'success', result: 42, attempts: 2 }) + }) + + it('returns child-owned terminal failures unchanged', async () => { + const failure = Object.freeze({ code: 'CHILD_FAILURE' }) + await expect( + pollOracleEpmJob({ + read: async () => ({ status: 'FAILED' }), + classify: () => ({ state: 'failure', error: failure }), + maxWaitMs: 1_000, + cleanupReserveMs: 10, + maxAttempts: 1, + initialDelayMs: 1, + maxDelayMs: 1, + }) + ).resolves.toEqual({ state: 'failure', error: failure, attempts: 1 }) + }) + + it('rejects invalid classifier output and attempt exhaustion', async () => { + await expect( + pollOracleEpmJob({ + read: async () => ({}), + classify: () => ({ state: 'unknown' }) as never, + maxWaitMs: 1_000, + cleanupReserveMs: 10, + maxAttempts: 1, + initialDelayMs: 1, + maxDelayMs: 1, + }) + ).rejects.toThrow('classifier') + await expect( + pollOracleEpmJob({ + read: async () => ({}), + classify: () => ({ state: 'pending' }), + maxWaitMs: 1_000, + cleanupReserveMs: 10, + maxAttempts: 1, + initialDelayMs: 1, + maxDelayMs: 1, + }) + ).rejects.toThrow('attempt limit') + }) + + it('honors caller aborts and cleanup reserve', async () => { + const controller = new AbortController() + controller.abort(new DOMException('user', 'AbortError')) + await expect( + pollOracleEpmJob({ + read: async () => ({}), + classify: () => ({ state: 'pending' }), + signal: controller.signal, + maxWaitMs: 1_000, + cleanupReserveMs: 10, + maxAttempts: 2, + initialDelayMs: 1, + maxDelayMs: 1, + }) + ).rejects.toMatchObject({ name: 'AbortError' }) + await expect( + pollOracleEpmJob({ + read: async () => ({}), + classify: () => ({ state: 'pending' }), + deadlineAt: new Date(Date.now() + 5), + maxWaitMs: 1_000, + cleanupReserveMs: 10, + maxAttempts: 2, + initialDelayMs: 1, + maxDelayMs: 1, + }) + ).rejects.toMatchObject({ name: 'TimeoutError' }) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm/jobs.ts b/apps/sim/lib/internal/oracle-epm/jobs.ts new file mode 100644 index 00000000000..61add7331e4 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/jobs.ts @@ -0,0 +1,126 @@ +import { interruptibleSleep } from '@sim/utils/helpers' +import { backoffWithJitter } from '@sim/utils/retry' +import { getExecutionDeadlineAt } from '@/lib/core/execution-limits' + +/** Child-owned interpretation of one provider job snapshot. */ +export type OracleEpmPollClassification = + | { readonly state: 'pending' } + | { readonly state: 'success'; readonly result: TResult } + | { readonly state: 'failure'; readonly error: TFailure } + +/** Bounded scheduling policy plus child-owned read and classification callbacks. */ +export interface OracleEpmPollOptions { + readonly read: (signal: AbortSignal) => Promise + readonly classify: (snapshot: TSnapshot) => OracleEpmPollClassification + readonly signal?: AbortSignal + readonly deadlineAt?: Date + readonly maxWaitMs: number + readonly cleanupReserveMs: number + readonly maxAttempts: number + readonly initialDelayMs: number + readonly maxDelayMs: number +} + +/** Terminal child result annotated with the scheduler attempt count. */ +export type OracleEpmPollResult = + | { readonly state: 'success'; readonly result: TResult; readonly attempts: number } + | { readonly state: 'failure'; readonly error: TFailure; readonly attempts: number } + +function validateOptions(options: OracleEpmPollOptions): void { + if ( + !Number.isInteger(options.maxWaitMs) || + options.maxWaitMs < 1 || + options.maxWaitMs > 24 * 60 * 60 * 1_000 || + !Number.isInteger(options.cleanupReserveMs) || + options.cleanupReserveMs < 0 || + options.cleanupReserveMs >= options.maxWaitMs || + !Number.isInteger(options.maxAttempts) || + options.maxAttempts < 1 || + options.maxAttempts > 10_000 || + !Number.isInteger(options.initialDelayMs) || + options.initialDelayMs < 1 || + !Number.isInteger(options.maxDelayMs) || + options.maxDelayMs < options.initialDelayMs || + options.maxDelayMs > 60_000 + ) { + throw new Error('Oracle EPM polling policy is invalid') + } +} + +function assertClassification( + value: unknown +): asserts value is OracleEpmPollClassification { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error('Oracle EPM polling classifier returned an invalid state') + } + const record = value as Record + if (record.state === 'pending') { + if (Object.keys(record).some((key) => key !== 'state')) { + throw new Error('Oracle EPM polling classifier returned an invalid pending state') + } + return + } + if (record.state === 'success' && Object.hasOwn(record, 'result')) return + if (record.state === 'failure' && Object.hasOwn(record, 'error')) return + throw new Error('Oracle EPM polling classifier returned an invalid state') +} + +/** + * Runs child-supplied polling semantics inside caller and execution deadlines. + * The scheduler knows nothing about Oracle job ids, statuses, results, or cancellation. + */ +export async function pollOracleEpmJob( + options: OracleEpmPollOptions +): Promise> { + validateOptions(options as OracleEpmPollOptions) + const startedAt = Date.now() + const executionDeadline = getExecutionDeadlineAt(options.signal)?.getTime() + const explicitDeadline = options.deadlineAt?.getTime() + const candidates = [ + startedAt + options.maxWaitMs, + ...(executionDeadline === undefined ? [] : [executionDeadline]), + ...(explicitDeadline === undefined ? [] : [explicitDeadline]), + ] + const deadline = Math.min(...candidates) - options.cleanupReserveMs + if (!Number.isFinite(deadline) || deadline <= startedAt) { + throw new DOMException('Oracle EPM polling deadline exceeded', 'TimeoutError') + } + + const deadlineSignal = AbortSignal.timeout(Math.max(1, deadline - startedAt)) + const signal = options.signal ? AbortSignal.any([options.signal, deadlineSignal]) : deadlineSignal + + for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) { + if (signal.aborted || Date.now() >= deadline) { + throw ( + signal.reason ?? new DOMException('Oracle EPM polling deadline exceeded', 'TimeoutError') + ) + } + const snapshot = await options.read(signal) + const classification: unknown = options.classify(snapshot) + assertClassification(classification) + if (classification.state === 'success') { + return Object.freeze({ + state: 'success' as const, + result: classification.result, + attempts: attempt, + }) + } + if (classification.state === 'failure') { + return Object.freeze({ + state: 'failure' as const, + error: classification.error, + attempts: attempt, + }) + } + if (attempt === options.maxAttempts) break + const delay = backoffWithJitter(attempt, null, { + baseMs: options.initialDelayMs, + maxMs: options.maxDelayMs, + }) + if (Date.now() + delay >= deadline) { + throw new DOMException('Oracle EPM polling deadline exceeded', 'TimeoutError') + } + await interruptibleSleep(delay, signal) + } + throw new Error('Oracle EPM polling attempt limit exceeded') +} diff --git a/apps/sim/lib/internal/oracle-epm/links.test.ts b/apps/sim/lib/internal/oracle-epm/links.test.ts new file mode 100644 index 00000000000..f8536f27fe6 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/links.test.ts @@ -0,0 +1,60 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { oracleEpmLiteral } from '@/lib/internal/oracle-epm/endpoint' +import { getOracleEpmReturnedLinkPolicy } from '@/lib/internal/oracle-epm/links' +import { defineOracleEpmRouteSpace } from '@/lib/internal/oracle-epm/route-space' +import type { OracleEpmReturnedLinkPolicy } from '@/lib/internal/oracle-epm/types' + +const routes = defineOracleEpmRouteSpace({ + context: ['Synthetic', 'rest'], + allowedVersions: ['v3'], +}) + +describe('Oracle EPM returned-link declarations', () => { + it('binds a frozen policy to an endpoint', () => { + const endpoint = routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('download')], + body: 'none', + response: 'stream', + timeoutMs: 2_000, + maxResponseBytes: 1_024, + }) + const policy = routes.defineReturnedLinkPolicy({ + relation: 'download', + method: 'GET', + endpoint, + preserveGatewayBasePath: true, + }) + expect(Object.isFrozen(policy)).toBe(true) + expect(getOracleEpmReturnedLinkPolicy(policy)).toMatchObject({ + relation: 'download', + method: 'GET', + version: 'v3', + }) + }) + + it('rejects method overrides and forged policies', () => { + const endpoint = routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [], + body: 'none', + response: 'empty', + timeoutMs: 1_000, + maxResponseBytes: 1, + }) + expect(() => + routes.defineReturnedLinkPolicy({ + relation: 'next', + method: 'POST', + endpoint, + preserveGatewayBasePath: true, + }) + ).toThrow('match') + expect(() => getOracleEpmReturnedLinkPolicy({} as OracleEpmReturnedLinkPolicy)).toThrow( + 'not a valid declaration' + ) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm/links.ts b/apps/sim/lib/internal/oracle-epm/links.ts new file mode 100644 index 00000000000..4ddad7b25c3 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/links.ts @@ -0,0 +1,125 @@ +import { + getOracleEpmEndpoint, + type OracleEpmEndpointDefinition, +} from '@/lib/internal/oracle-epm/endpoint' +import { getOracleEpmRouteSpace } from '@/lib/internal/oracle-epm/route-space' +import type { + OracleEpmPathPart, + OracleEpmQueryParameter, + OracleEpmReturnedLinkPolicy, + OracleEpmReturnedLinkPolicyDeclaration, + OracleEpmRouteSpace, +} from '@/lib/internal/oracle-epm/types' + +const policies = new WeakMap() +const RELATION = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/ + +/** Internal frozen link policy available only after runtime-brand validation. */ +export interface OracleEpmReturnedLinkPolicyDefinition { + readonly routeSpace: OracleEpmRouteSpace + readonly relation: string + readonly method: OracleEpmReturnedLinkPolicyDeclaration['method'] + readonly version: string + readonly path: readonly OracleEpmPathPart[] + readonly query: Readonly> + readonly preserveGatewayBasePath: boolean + readonly endpoint: OracleEpmEndpointDefinition +} + +/** Creates a declarative, immutable returned-link policy during module initialization. */ +export function defineOracleEpmReturnedLinkPolicy( + routeSpace: OracleEpmRouteSpace, + declaration: OracleEpmReturnedLinkPolicyDeclaration +): OracleEpmReturnedLinkPolicy { + const route = getOracleEpmRouteSpace(routeSpace) + if (!RELATION.test(declaration.relation)) + throw new Error('Oracle EPM returned-link relation is invalid') + if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].includes(declaration.method)) + throw new Error('Oracle EPM returned-link method is invalid') + + let version: string + let path: readonly OracleEpmPathPart[] + let query: Readonly> + let endpointDefinition: OracleEpmEndpointDefinition + if (declaration.endpoint) { + if ( + declaration.version || + declaration.path || + declaration.query || + declaration.response || + declaration.timeoutMs || + declaration.maxResponseBytes || + declaration.errors + ) { + throw new Error('Endpoint-bound Oracle EPM link policies cannot override route structure') + } + const endpoint = getOracleEpmEndpoint(declaration.endpoint) + if ( + endpoint.routeSpace !== routeSpace || + endpoint.method !== declaration.method || + endpoint.body !== 'none' + ) { + throw new Error('Oracle EPM returned-link policy endpoint does not match its route or method') + } + endpointDefinition = endpoint + version = endpoint.version + path = endpoint.path + query = endpoint.query ?? {} + } else { + if ( + !declaration.version || + !declaration.path || + !declaration.response || + declaration.timeoutMs === undefined || + declaration.maxResponseBytes === undefined + ) { + throw new Error( + 'Route-bound Oracle EPM link policies require route and response declarations' + ) + } + // Reuse the endpoint validator without retaining the synthetic endpoint. + const synthetic = routeSpace.defineEndpoint({ + method: declaration.method, + version: declaration.version, + path: declaration.path, + query: declaration.query, + body: 'none', + response: declaration.response, + timeoutMs: declaration.timeoutMs, + maxResponseBytes: declaration.maxResponseBytes, + errors: declaration.errors, + }) + const endpoint = getOracleEpmEndpoint(synthetic) + endpointDefinition = endpoint + version = endpoint.version + path = endpoint.path + query = endpoint.query ?? {} + } + if (!route.allowedVersions.includes(version)) + throw new Error('Oracle EPM returned-link version is not declared') + + const policy = Object.freeze({}) as OracleEpmReturnedLinkPolicy + policies.set( + policy, + Object.freeze({ + routeSpace, + relation: declaration.relation, + method: declaration.method, + version, + path, + query, + preserveGatewayBasePath: declaration.preserveGatewayBasePath, + endpoint: endpointDefinition, + }) + ) + return policy +} + +/** Reads a link policy only after its runtime brand is verified. */ +export function getOracleEpmReturnedLinkPolicy( + policy: OracleEpmReturnedLinkPolicy +): OracleEpmReturnedLinkPolicyDefinition { + const definition = policies.get(policy) + if (!definition) throw new Error('Oracle EPM returned-link policy is not a valid declaration') + return definition +} diff --git a/apps/sim/lib/internal/oracle-epm/route-space.test.ts b/apps/sim/lib/internal/oracle-epm/route-space.test.ts new file mode 100644 index 00000000000..259d3670e94 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/route-space.test.ts @@ -0,0 +1,40 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { + defineOracleEpmRouteSpace, + getOracleEpmRouteSpace, +} from '@/lib/internal/oracle-epm/route-space' +import type { OracleEpmRouteSpace } from '@/lib/internal/oracle-epm/types' + +describe('defineOracleEpmRouteSpace', () => { + it('supports unrelated child contexts and case-sensitive versions without foundation edits', () => { + const lower = defineOracleEpmRouteSpace({ + context: ['SyntheticAlpha', 'rest'], + allowedVersions: ['v3'], + }) + const upper = defineOracleEpmRouteSpace({ + context: ['synthetic-beta', 'api'], + allowedVersions: ['V1'], + }) + + expect(lower.allowedVersions).toEqual(['v3']) + expect(upper.allowedVersions).toEqual(['V1']) + expect(Object.isFrozen(lower)).toBe(true) + expect(Object.isFrozen(lower.context)).toBe(true) + }) + + it.each([ + { context: [], allowedVersions: ['v1'] }, + { context: ['../admin'], allowedVersions: ['v1'] }, + { context: ['rest'], allowedVersions: ['v1', 'v1'] }, + { context: ['rest'], allowedVersions: ['v/1'] }, + ])('rejects invalid static declarations', (declaration) => { + expect(() => defineOracleEpmRouteSpace(declaration)).toThrow() + }) + + it('rejects forged route spaces', () => { + expect(() => getOracleEpmRouteSpace({} as OracleEpmRouteSpace)).toThrow( + 'not a valid declaration' + ) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm/route-space.ts b/apps/sim/lib/internal/oracle-epm/route-space.ts new file mode 100644 index 00000000000..62c4071832c --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/route-space.ts @@ -0,0 +1,74 @@ +import { defineOracleEpmEndpoint } from '@/lib/internal/oracle-epm/endpoint' +import { defineOracleEpmReturnedLinkPolicy } from '@/lib/internal/oracle-epm/links' +import type { + OracleEpmEndpointDeclaration, + OracleEpmReturnedLinkPolicy, + OracleEpmReturnedLinkPolicyDeclaration, + OracleEpmRouteSpace, +} from '@/lib/internal/oracle-epm/types' + +const MAX_CONTEXT_SEGMENTS = 16 +const MAX_SEGMENT_BYTES = 255 +const STATIC_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._~-]*$/ +const routeSpaces = new WeakMap() + +/** Internal frozen route metadata available only after runtime-brand validation. */ +export interface OracleEpmRouteSpaceDefinition { + readonly context: readonly string[] + readonly allowedVersions: readonly string[] +} + +function validateStaticSegments(values: readonly string[], label: string): void { + if (!Array.isArray(values) || !values.length || values.length > MAX_CONTEXT_SEGMENTS) { + throw new Error(`${label} declaration is invalid`) + } + const seen = new Set() + for (const value of values) { + if ( + typeof value !== 'string' || + !STATIC_SEGMENT.test(value) || + value === '.' || + value === '..' || + Buffer.byteLength(value, 'utf8') > MAX_SEGMENT_BYTES || + seen.has(value) + ) { + throw new Error(`${label} declaration is invalid`) + } + seen.add(value) + } +} + +/** + * Defines a child-owned Oracle EPM context and its exact, case-sensitive API + * versions. Declarations are static code and are validated immediately. + */ +export function defineOracleEpmRouteSpace(declaration: { + readonly context: readonly string[] + readonly allowedVersions: readonly string[] +}): OracleEpmRouteSpace { + validateStaticSegments(declaration.context, 'Oracle EPM route context') + validateStaticSegments(declaration.allowedVersions, 'Oracle EPM route version') + const context = Object.freeze([...declaration.context]) + const allowedVersions = Object.freeze([...declaration.allowedVersions]) + const routeSpace = Object.freeze({ + context, + allowedVersions, + defineEndpoint: (endpointDeclaration: OracleEpmEndpointDeclaration) => + defineOracleEpmEndpoint(routeSpace, endpointDeclaration), + defineReturnedLinkPolicy: ( + policyDeclaration: OracleEpmReturnedLinkPolicyDeclaration + ): OracleEpmReturnedLinkPolicy => + defineOracleEpmReturnedLinkPolicy(routeSpace, policyDeclaration), + }) as OracleEpmRouteSpace + routeSpaces.set(routeSpace, Object.freeze({ context, allowedVersions })) + return routeSpace +} + +/** Reads a route-space declaration only after its runtime brand is verified. */ +export function getOracleEpmRouteSpace( + routeSpace: OracleEpmRouteSpace +): OracleEpmRouteSpaceDefinition { + const definition = routeSpaces.get(routeSpace) + if (!definition) throw new Error('Oracle EPM route space is not a valid declaration') + return definition +} diff --git a/apps/sim/lib/internal/oracle-epm/types.ts b/apps/sim/lib/internal/oracle-epm/types.ts new file mode 100644 index 00000000000..e2d6370361a --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm/types.ts @@ -0,0 +1,164 @@ +import type { HttpMethod } from '@/tools/types' + +declare const destinationBrand: unique symbol +declare const routeSpaceBrand: unique symbol +declare const endpointBrand: unique symbol +declare const linkPolicyBrand: unique symbol +declare const validatedLinkBrand: unique symbol + +/** A validated HTTPS environment URL supplied by one credential. */ +export interface OracleEpmDestination { + readonly [destinationBrand]: true +} + +/** One static or individually encoded segment in a declared endpoint path. */ +export type OracleEpmPathPart = + | { readonly kind: 'literal'; readonly value: string } + | { + readonly kind: 'parameter' + readonly name: string + readonly maxBytes: number + readonly pattern?: RegExp + } + +/** Bounded scalar query input admitted by an endpoint or returned-link policy. */ +export type OracleEpmQueryParameter = + | { + readonly kind: 'string' + readonly required?: boolean + readonly maxBytes: number + readonly pattern?: RegExp + } + | { + readonly kind: 'integer' + readonly required?: boolean + readonly minimum: number + readonly maximum: number + } + | { readonly kind: 'boolean'; readonly required?: boolean } + +/** Bounded header input whose wire name is fixed by trusted source code. */ +export interface OracleEpmHeaderDeclaration { + readonly name: string + readonly required?: boolean + readonly maxBytes: number + readonly pattern?: RegExp +} + +/** Body encodings understood by the guarded client. */ +export type OracleEpmBodyMode = 'none' | 'json' | 'stream' +/** Response projections understood by the guarded client. */ +export type OracleEpmResponseMode = 'empty' | 'json' | 'stream' + +/** Declarative allowlist for safe provider error metadata. */ +export interface OracleEpmErrorPolicyDeclaration { + /** Static JSON property path containing a documented provider error code. */ + readonly providerCodePath?: readonly string[] + /** Exact provider codes safe to expose outside the transport. */ + readonly allowedProviderCodes?: readonly string[] + /** Response headers that may carry a non-secret request/correlation id. */ + readonly correlationHeaders?: readonly string[] +} + +/** Complete static transport contract owned by one product child. */ +export interface OracleEpmEndpointDeclaration { + readonly method: HttpMethod + readonly version: string + readonly path: readonly OracleEpmPathPart[] + readonly query?: Readonly> + readonly headers?: Readonly> + readonly body: OracleEpmBodyMode + readonly response: OracleEpmResponseMode + readonly timeoutMs: number + /** Required for request bodies and forbidden for bodyless endpoints. */ + readonly maxRequestBytes?: number + readonly maxResponseBytes: number + readonly retry?: { + readonly maxAttempts: number + readonly statuses: readonly number[] + readonly initialDelayMs: number + readonly maxDelayMs: number + } + readonly errors?: OracleEpmErrorPolicyDeclaration +} + +/** A child-owned, validated and immutable Oracle EPM route declaration. */ +export interface OracleEpmRouteSpace { + readonly [routeSpaceBrand]: true + readonly context: readonly string[] + readonly allowedVersions: readonly string[] + defineEndpoint(declaration: OracleEpmEndpointDeclaration): OracleEpmEndpoint + defineReturnedLinkPolicy( + declaration: OracleEpmReturnedLinkPolicyDeclaration + ): OracleEpmReturnedLinkPolicy +} + +/** A fully static request contract created from a route space. */ +export interface OracleEpmEndpoint { + readonly [endpointBrand]: true +} + +/** Static route, method, relation, and response rules for one returned link. */ +export interface OracleEpmReturnedLinkPolicyDeclaration { + readonly relation: string + readonly method: HttpMethod + /** Bind to this endpoint, or use route/version/path below. */ + readonly endpoint?: OracleEpmEndpoint + readonly version?: string + readonly path?: readonly OracleEpmPathPart[] + readonly query?: Readonly> + /** Required for route-bound policies and inherited by endpoint-bound policies. */ + readonly response?: OracleEpmResponseMode + readonly timeoutMs?: number + readonly maxResponseBytes?: number + readonly errors?: OracleEpmErrorPolicyDeclaration + readonly preserveGatewayBasePath: boolean +} + +/** A reviewed, declarative contract for one provider-returned link. */ +export interface OracleEpmReturnedLinkPolicy { + readonly [linkPolicyBrand]: true +} + +/** An opaque capability. Only the client that validated it can consume it. */ +export interface OracleEpmValidatedLink { + readonly [validatedLinkBrand]: true +} + +/** Dynamic values accepted by a previously declared endpoint. */ +export interface OracleEpmRequestInput { + readonly pathParams?: Readonly> + readonly query?: Readonly> + readonly headers?: Readonly> + readonly json?: unknown + readonly stream?: Uint8Array + readonly signal?: AbortSignal +} + +/** Sanitized successful JSON response. */ +export interface OracleEpmJsonResponse { + readonly status: number + readonly data: unknown + readonly correlationId?: string +} + +/** Sanitized successful bodyless response. */ +export interface OracleEpmEmptyResponse { + readonly status: number + readonly correlationId?: string +} + +/** Bounded response stream with only safe, explicitly projected metadata. */ +export interface OracleEpmStreamResponse { + readonly status: number + readonly body: ReadableStream + readonly contentLength?: number + readonly contentType?: string + readonly correlationId?: string +} + +/** Successful response projections returned by the guarded client. */ +export type OracleEpmClientResponse = + | OracleEpmJsonResponse + | OracleEpmEmptyResponse + | OracleEpmStreamResponse diff --git a/apps/sim/lib/oauth/credential-service.test.ts b/apps/sim/lib/oauth/credential-service.test.ts index 337b56aa435..eab5027e42f 100644 --- a/apps/sim/lib/oauth/credential-service.test.ts +++ b/apps/sim/lib/oauth/credential-service.test.ts @@ -7,6 +7,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ coalesceLocally: vi.fn(), + clientCredentialMinter: vi.fn(), + decryptSecret: vi.fn(), getFreshestSlackChain: vi.fn(), getRecentTerminalError: vi.fn(), logger: { @@ -33,6 +35,19 @@ vi.mock('@/lib/concurrency/leader-lock', () => ({ withLeaderLock: mocks.withLeaderLock, })) +vi.mock('@/lib/core/security/encryption', () => ({ + decryptSecret: mocks.decryptSecret, +})) + +vi.mock('@/lib/credentials/client-credential-accounts/server', async (importOriginal) => { + const actual = + await importOriginal() + return { + ...actual, + getClientCredentialAccountMinter: vi.fn(() => mocks.clientCredentialMinter), + } +}) + vi.mock('@/lib/oauth/instagram', () => ({ isInstagramProvider: vi.fn(() => false), shouldProactivelyRefreshInstagramToken: vi.fn(() => false), @@ -64,7 +79,10 @@ vi.mock('@/lib/oauth/terminal-errors', () => ({ markCredentialDead: vi.fn(), })) -import { resolveCredentialTokenBundle } from '@/lib/oauth/credential-service' +import { + resolveCredentialTokenBundle, + resolveServiceAccountToken, +} from '@/lib/oauth/credential-service' const RAW_CREDENTIAL_ID = 'credential-raw-secret-id' const RAW_ACCOUNT_ID = 'account-raw-secret-id' @@ -200,3 +218,64 @@ describe('resolveCredentialTokenBundle selector privacy', () => { expect(slack.logs).toContain(RAW_PROVIDER_ERROR) }) }) + +describe('Oracle EPM client-credential token cache', () => { + const providerId = 'oracle-epm-service-account' + const blob = JSON.stringify({ + type: 'client_credential_account', + providerId, + clientId: 'integration.user@example.com', + clientSecret: 'password', + orgId: 'https://epm.example.com', + }) + + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + mocks.coalesceLocally.mockImplementation( + async (_key: string, producer: () => Promise) => producer() + ) + mocks.decryptSecret.mockResolvedValue({ decrypted: blob }) + }) + + it('reuses the conservative synthetic token while its safety window remains', async () => { + const credentialId = 'oracle-epm-cache-credential' + const encrypted = 'cache-secret-fingerprint-000000000000000000000000000000000' + queueTableRows(credential, [{ encryptedServiceAccountKey: encrypted }]) + queueTableRows(credential, [{ encryptedServiceAccountKey: encrypted }]) + mocks.clientCredentialMinter.mockResolvedValue({ + accessToken: 'basic-token', + expiresInSeconds: 600, + instanceUrl: 'https://epm.example.com', + }) + + await expect(resolveServiceAccountToken(credentialId, providerId)).resolves.toMatchObject({ + accessToken: 'basic-token', + }) + await expect(resolveServiceAccountToken(credentialId, providerId)).resolves.toMatchObject({ + accessToken: 'basic-token', + }) + expect(mocks.clientCredentialMinter).toHaveBeenCalledTimes(1) + }) + + it('invalidates the cached token immediately when encrypted credentials rotate', async () => { + const credentialId = 'oracle-epm-rotation-credential' + queueTableRows(credential, [ + { encryptedServiceAccountKey: 'old-secret-fingerprint-000000000000000000000000000000000' }, + ]) + queueTableRows(credential, [ + { encryptedServiceAccountKey: 'new-secret-fingerprint-000000000000000000000000000000000' }, + ]) + mocks.clientCredentialMinter + .mockResolvedValueOnce({ accessToken: 'old-token', expiresInSeconds: 600 }) + .mockResolvedValueOnce({ accessToken: 'new-token', expiresInSeconds: 600 }) + + await expect(resolveServiceAccountToken(credentialId, providerId)).resolves.toMatchObject({ + accessToken: 'old-token', + }) + await expect(resolveServiceAccountToken(credentialId, providerId)).resolves.toMatchObject({ + accessToken: 'new-token', + }) + expect(mocks.clientCredentialMinter).toHaveBeenCalledTimes(2) + }) +}) diff --git a/apps/sim/lib/oauth/token-resolution.test.ts b/apps/sim/lib/oauth/token-resolution.test.ts index e06ee3c9c8d..99aa1a3d9ac 100644 --- a/apps/sim/lib/oauth/token-resolution.test.ts +++ b/apps/sim/lib/oauth/token-resolution.test.ts @@ -8,6 +8,7 @@ const { mockCaptureServerEvent, mockExecuteManagedToken, mockGetCredential, + mockGetServiceConfigByServiceId, mockGetToolMetadata, mockRecordAudit, mockRefreshTokenIfNeeded, @@ -18,6 +19,7 @@ const { mockCaptureServerEvent: vi.fn(), mockExecuteManagedToken: vi.fn(), mockGetCredential: vi.fn(), + mockGetServiceConfigByServiceId: vi.fn(), mockGetToolMetadata: vi.fn(), mockRecordAudit: vi.fn(), mockRefreshTokenIfNeeded: vi.fn(), @@ -78,7 +80,14 @@ vi.mock('@/tools/metadata', () => ({ })) vi.mock('@/lib/oauth/utils', () => ({ + credentialProviderMatchesService: ( + credentialProviderId: string, + service: { providerId: string; serviceAccountProviderId?: string } + ) => + credentialProviderId === service.providerId || + credentialProviderId === service.serviceAccountProviderId, getCanonicalScopesForProvider: vi.fn().mockReturnValue([]), + getServiceConfigByServiceId: mockGetServiceConfigByServiceId, })) import { OrchestrationError } from '@/lib/core/orchestration/types' @@ -351,6 +360,10 @@ describe('resolveCredentialAccessToken', () => { mockGetToolMetadata.mockReturnValue({ oauth: { required: true, provider: 'google', requiredScopes: ['scope-a'] }, }) + mockGetServiceConfigByServiceId.mockReturnValue({ + providerId: 'google', + serviceAccountProviderId: 'google-service-account', + }) }) it('authenticates and delegates non-managed credentials without a second account lookup', async () => { @@ -411,6 +424,102 @@ describe('resolveCredentialAccessToken', () => { }) }) + it('rejects a credential whose provider does not match the tool service', async () => { + mockResolveOAuthAccountId.mockResolvedValue({ + accountId: 'account-1', + credentialId: 'credential-1', + credentialType: 'service_account', + providerId: 'oracle-epm-service-account', + usedCredentialTable: true, + }) + + const result = await resolveCredentialAccessToken({ + requestId: 'req-1', + credentialId: 'credential-1', + toolId: 'gmail_send', + authenticate, + }) + + expect(result).toEqual({ + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + }) + expect(authenticate).not.toHaveBeenCalled() + expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() + }) + + it('accepts a shared service-account provider registered by the tool service', async () => { + mockResolveOAuthAccountId.mockResolvedValue({ + accountId: 'credential-1', + credentialId: 'credential-1', + credentialType: 'service_account', + providerId: 'oracle-epm-service-account', + usedCredentialTable: true, + }) + mockGetServiceConfigByServiceId.mockReturnValue({ + providerId: 'synthetic-oracle-child', + serviceAccountProviderId: 'oracle-epm-service-account', + }) + mockAuthorizeCredentialUseForAuth.mockResolvedValue({ + ok: true, + requesterUserId: 'user-1', + credentialOwnerUserId: 'owner-1', + workspaceId: 'ws-1', + resolvedCredentialId: 'credential-1', + }) + mockResolveServiceAccountToken.mockResolvedValue({ + accessToken: 'basic-token', + instanceUrl: 'https://epm.example.com', + }) + + const result = await resolveCredentialAccessToken({ + requestId: 'req-1', + credentialId: 'credential-1', + toolId: 'synthetic_oracle_tool', + authenticate, + }) + + expect(result).toEqual({ + ok: true, + token: { + accessToken: 'basic-token', + credentialType: 'service_account', + instanceUrl: 'https://epm.example.com', + }, + }) + }) + + it('rejects a mismatched OAuth account after loading its authoritative provider', async () => { + mockResolveOAuthAccountId.mockResolvedValue({ + accountId: 'account-1', + usedCredentialTable: true, + }) + mockAuthorizeCredentialUseForAuth.mockResolvedValue({ + ok: true, + requesterUserId: 'user-1', + credentialOwnerUserId: 'owner-1', + resolvedCredentialId: 'account-1', + }) + mockGetCredential.mockResolvedValue({ providerId: 'salesforce' }) + + const result = await resolveCredentialAccessToken({ + requestId: 'req-1', + credentialId: 'credential-1', + toolId: 'gmail_send', + authenticate, + }) + + expect(result).toEqual({ + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + }) + expect(mockRefreshTokenIfNeeded).not.toHaveBeenCalled() + }) + it('rejects a managed credential when no delegation resolver is wired', async () => { mockResolveOAuthAccountId.mockResolvedValue(MANAGED_RESOLVED) diff --git a/apps/sim/lib/oauth/token-resolution.ts b/apps/sim/lib/oauth/token-resolution.ts index dfd1f682b5b..30788aee9bf 100644 --- a/apps/sim/lib/oauth/token-resolution.ts +++ b/apps/sim/lib/oauth/token-resolution.ts @@ -24,7 +24,12 @@ import { MICROSOFT_DATAVERSE_PROVIDER_ID, } from '@/lib/oauth/microsoft-dataverse' import { extractSalesforceInstanceUrl, isSalesforceOAuthProviderId } from '@/lib/oauth/salesforce' -import { getCanonicalScopesForProvider } from '@/lib/oauth/utils' +import { + credentialProviderMatchesService, + getCanonicalScopesForProvider, + getServiceConfigByServiceId, + type ServiceProviderIdentity, +} from '@/lib/oauth/utils' import { captureServerEvent } from '@/lib/posthog/server' import { getToolMetadata } from '@/tools/metadata' import { extractZohoDeskBaseFromScope } from '@/tools/zoho_desk/host-allowlist' @@ -59,6 +64,8 @@ export interface ResolveCredentialTokenInput { auditRequest?: CredentialAuditRequest /** Credential lookup already performed by {@link resolveCredentialAccessToken}'s dispatch. */ resolvedCredential: ResolvedCredential | null + /** Trusted tool-service identity used to reject cross-service credentials. */ + expectedService?: ServiceProviderIdentity } export type ResolveCredentialTokenResult = @@ -208,6 +215,18 @@ export async function resolveCredentialToken( }) if (resolved?.credentialType === 'service_account' && resolved.credentialId) { + if ( + input.expectedService && + (!resolved.providerId || + !credentialProviderMatchesService(resolved.providerId, input.expectedService)) + ) { + return { + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + } + } if (!authz.ok) { return { ok: false, status: 403, error: authz.error || 'Unauthorized' } } @@ -298,6 +317,17 @@ export async function resolveCredentialToken( if (!credential) { return { ok: false, status: 404, error: 'Credential not found' } } + if ( + input.expectedService && + !credentialProviderMatchesService(credential.providerId, input.expectedService) + ) { + return { + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + } + } return completeOAuthCredentialToken({ requestId, @@ -344,6 +374,33 @@ export async function resolveCredentialAccessToken( const { requestId, credentialId, toolId, auditRequest } = input const resolved = credentialId ? await resolveOAuthAccountId(credentialId) : null + const toolMetadata = toolId ? getToolMetadata(toolId) : undefined + const expectedService = toolMetadata?.oauth?.required + ? getServiceConfigByServiceId(toolMetadata.oauth.provider) + : null + + if (credentialId && toolId && resolved?.credentialType !== 'managed_oauth' && !expectedService) { + return { + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + } + } + + if (resolved?.credentialType !== 'managed_oauth' && resolved?.providerId && toolId) { + if ( + !expectedService || + !credentialProviderMatchesService(resolved.providerId, expectedService) + ) { + return { + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + } + } + } if (resolved?.credentialType !== 'managed_oauth' || !resolved.credentialId) { const auth = await input.authenticate() @@ -361,6 +418,7 @@ export async function resolveCredentialAccessToken( callerUserId: input.callerUserId, auditRequest, resolvedCredential: resolved, + expectedService: expectedService ?? undefined, }) } @@ -395,8 +453,8 @@ export async function resolveCredentialAccessToken( } } - const toolMetadata = getToolMetadata(toolId) - if (!toolMetadata?.oauth?.required) { + const managedToolMetadata = toolMetadata + if (!managedToolMetadata?.oauth?.required) { logger.error(`[${requestId}] Tool is not configured for managed OAuth`, { toolId }) return { ok: false, @@ -406,11 +464,12 @@ export async function resolveCredentialAccessToken( } } const requiredScopes = - toolMetadata.oauth.requiredScopes ?? getCanonicalScopesForProvider(toolMetadata.oauth.provider) + managedToolMetadata.oauth.requiredScopes ?? + getCanonicalScopesForProvider(managedToolMetadata.oauth.provider) if (requiredScopes.length === 0) { logger.error(`[${requestId}] Tool has no trusted OAuth scope policy`, { toolId, - providerId: toolMetadata.oauth.provider, + providerId: managedToolMetadata.oauth.provider, }) return { ok: false, @@ -425,7 +484,7 @@ export async function resolveCredentialAccessToken( principal, input: { credentialId: resolved.credentialId, - expectedProviderId: toolMetadata.oauth.provider, + expectedProviderId: managedToolMetadata.oauth.provider, requiredScopes, toolId, }, @@ -439,7 +498,7 @@ export async function resolveCredentialAccessToken( 'credential_used', { credential_type: 'managed_oauth', - provider_id: toolMetadata.oauth.provider, + provider_id: managedToolMetadata.oauth.provider, workspace_id: principal.workspaceId, }, { groups: { workspace: principal.workspaceId } } diff --git a/apps/sim/tools/shared/oracle-epm.test.ts b/apps/sim/tools/shared/oracle-epm.test.ts new file mode 100644 index 00000000000..f2018ef72ea --- /dev/null +++ b/apps/sim/tools/shared/oracle-epm.test.ts @@ -0,0 +1,38 @@ +/** @vitest-environment node */ +import { describe, expect, it, vi } from 'vitest' + +const { getServiceConfig } = vi.hoisted(() => ({ getServiceConfig: vi.fn() })) +vi.mock('@/lib/oauth/utils', () => ({ getServiceConfigByServiceId: getServiceConfig })) + +import { createOracleEpmAuthParameters } from '@/tools/shared/oracle-epm' + +describe('createOracleEpmAuthParameters', () => { + it('returns a fresh deeply frozen bundle tied to the child service id', () => { + getServiceConfig.mockReturnValue({ + providerId: 'synthetic-provider', + serviceAccountProviderId: 'oracle-epm-service-account', + }) + const first = createOracleEpmAuthParameters({ serviceId: 'jira' }) + const second = createOracleEpmAuthParameters({ serviceId: 'jira' }) + + expect(first).not.toBe(second) + expect(first.params).not.toBe(second.params) + expect(first.oauth.provider).toBe('jira') + expect(first.oauth).toMatchObject({ + credentialKind: 'service-account', + authoritativeParams: ['instanceUrl'], + }) + expect(first.params.oauthCredential.visibility).toBe('user-only') + expect(first.params.accessToken.visibility).toBe('hidden') + expect(Object.isFrozen(first)).toBe(true) + expect(Object.isFrozen(first.params.oauthCredential)).toBe(true) + expect(Object.isFrozen(first.oauth.authoritativeParams)).toBe(true) + }) + + it('fails at declaration time when the service is absent or mapped elsewhere', () => { + getServiceConfig.mockReturnValue(null) + expect(() => createOracleEpmAuthParameters({ serviceId: 'jira' })).toThrow('not registered') + getServiceConfig.mockReturnValue({ serviceAccountProviderId: 'another-provider' }) + expect(() => createOracleEpmAuthParameters({ serviceId: 'jira' })).toThrow('not registered') + }) +}) diff --git a/apps/sim/tools/shared/oracle-epm.ts b/apps/sim/tools/shared/oracle-epm.ts new file mode 100644 index 00000000000..868c690f9e1 --- /dev/null +++ b/apps/sim/tools/shared/oracle-epm.ts @@ -0,0 +1,60 @@ +import { ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/credentials/client-credential-accounts/descriptors' +import type { OAuthService } from '@/lib/oauth' +import { getServiceConfigByServiceId } from '@/lib/oauth/utils' +import type { OAuthConfig, ParameterVisibility } from '@/tools/types' + +interface OracleEpmAuthParameter { + readonly type: 'string' + readonly required: boolean + readonly visibility: ParameterVisibility + readonly description: string +} + +export interface OracleEpmAuthParameters { + readonly params: Readonly<{ + oauthCredential: OracleEpmAuthParameter + accessToken: OracleEpmAuthParameter + instanceUrl: OracleEpmAuthParameter + }> + readonly oauth: Readonly +} + +/** + * Creates one child integration's service-account contract. Calling this from + * a child module validates its registered service mapping immediately. + */ +export function createOracleEpmAuthParameters(input: { + serviceId: OAuthService +}): OracleEpmAuthParameters { + const service = getServiceConfigByServiceId(input.serviceId) + if (service?.serviceAccountProviderId !== ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID) { + throw new Error('Oracle EPM service is not registered with the Oracle EPM credential provider') + } + + const oauthCredential = Object.freeze({ + type: 'string', + required: true, + visibility: 'user-only', + description: 'Oracle EPM integration-user credential', + } as const) + const accessToken = Object.freeze({ + type: 'string', + required: true, + visibility: 'hidden', + description: 'Credential-resolved Oracle EPM Basic authorization value', + } as const) + const instanceUrl = Object.freeze({ + type: 'string', + required: true, + visibility: 'hidden', + description: 'Credential-bound Oracle EPM environment URL', + } as const) + const params = Object.freeze({ oauthCredential, accessToken, instanceUrl }) + const oauth = Object.freeze({ + required: true, + provider: input.serviceId, + credentialKind: 'service-account', + authoritativeParams: Object.freeze(['instanceUrl'] as const), + }) + return Object.freeze({ params, oauth }) +} From 7ba3213c9fb042fdb4f9be2e26a845df73600d01 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 3 Sep 2026 18:51:07 -0700 Subject: [PATCH 02/20] fix(oracle-epm): harden guarded foundation --- .../credentials/orchestration/index.test.ts | 8 ++ .../lib/credentials/orchestration/index.ts | 12 ++- .../internal/oracle-epm/client.server.test.ts | 44 ++++++++++ .../lib/internal/oracle-epm/client.server.ts | 80 ++++++++++++++++++- .../internal/oracle-epm/destination.test.ts | 10 +++ .../lib/internal/oracle-epm/destination.ts | 50 +++++++----- .../lib/internal/oracle-epm/endpoint.test.ts | 40 ++++++++++ apps/sim/lib/internal/oracle-epm/endpoint.ts | 18 ++++- .../internal/oracle-epm/files.server.test.ts | 62 ++++++++++++++ .../lib/internal/oracle-epm/files.server.ts | 24 +++--- apps/sim/lib/internal/oracle-epm/jobs.test.ts | 40 ++++++++++ apps/sim/lib/internal/oracle-epm/jobs.ts | 49 ++++++++++-- apps/sim/lib/oauth/token-resolution.test.ts | 11 +-- apps/sim/lib/oauth/token-resolution.ts | 4 +- 14 files changed, 399 insertions(+), 53 deletions(-) diff --git a/apps/sim/lib/credentials/orchestration/index.test.ts b/apps/sim/lib/credentials/orchestration/index.test.ts index f702180f0d0..c7de7da67a3 100644 --- a/apps/sim/lib/credentials/orchestration/index.test.ts +++ b/apps/sim/lib/credentials/orchestration/index.test.ts @@ -286,6 +286,9 @@ describe('performUpdateCredential — service-account secret rotation', () => { it('carries the stored dataCenter forward for a client-credential reconnect', async () => { mockCredential({ providerId: 'zoho-desk-service-account', displayName: 'Acme Desk' }) mockIsClientCredentialAccountProviderId.mockReturnValue(true) + mockGetClientCredentialAccountDescriptor.mockReturnValue({ + fields: [{ id: 'dataCenter' }], + } as never) mockStoredBlob({ type: 'client_credential_account', dataCenter: 'eu' }) mockVerifyAndBuildServiceAccountSecret.mockResolvedValue({ providerId: 'zoho-desk-service-account', @@ -331,6 +334,7 @@ describe('performUpdateCredential — service-account secret rotation', () => { mockIsClientCredentialAccountProviderId.mockReturnValue(true) mockGetClientCredentialAccountDescriptor.mockReturnValue({ defaultAuthMethod: 'client_credentials', + fields: [], } as never) mockStoredBlob({ type: 'client_credential_account', @@ -420,6 +424,10 @@ describe('performUpdateCredential — service-account secret rotation', () => { displayName: 'Production EPM', }) mockIsClientCredentialAccountProviderId.mockReturnValue(true) + mockGetClientCredentialAccountDescriptor.mockReturnValue({ + fields: [{ id: 'orgId' }, { id: 'clientId' }, { id: 'clientSecret' }], + } as never) + mockStoredBlob({ type: 'client_credential_account' }) mockVerifyAndBuildServiceAccountSecret.mockResolvedValue({ providerId: 'oracle-epm-service-account', encryptedServiceAccountKey: 'new-cipher', diff --git a/apps/sim/lib/credentials/orchestration/index.ts b/apps/sim/lib/credentials/orchestration/index.ts index 47cc51e4927..19503cfbe7d 100644 --- a/apps/sim/lib/credentials/orchestration/index.ts +++ b/apps/sim/lib/credentials/orchestration/index.ts @@ -292,12 +292,16 @@ export async function updateCredentialRecord( // credential back to the US accounts server. Carry the stored value forward // when the caller did not supply one. const isClientCredentialProvider = isClientCredentialAccountProviderId(providerId) - const needsStoredDataCenter = params.dataCenter === undefined && isClientCredentialProvider + const clientCredentialDescriptor = isClientCredentialProvider + ? getClientCredentialAccountDescriptor(providerId) + : undefined + const storesDataCenter = Boolean( + clientCredentialDescriptor?.fields.some((field) => field.id === 'dataCenter') + ) + const needsStoredDataCenter = params.dataCenter === undefined && storesDataCenter // Only a multi-grant provider stores these, so single-grant ones must not // pay for a row read + decrypt that can only ever return undefined. - const isMultiGrantProvider = Boolean( - getClientCredentialAccountDescriptor(providerId)?.defaultAuthMethod - ) + const isMultiGrantProvider = Boolean(clientCredentialDescriptor?.defaultAuthMethod) const needsStoredAuthMethod = params.authMethod === undefined && isMultiGrantProvider const needsStoredUsername = params.username === undefined && isMultiGrantProvider diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index 54d33bfe890..d91b181f9a7 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -135,6 +135,17 @@ describe('Oracle EPM guarded client', () => { expect(mockSecureFetch.mock.calls[0][0]).toContain('%252e%252e%252fadmin') }) + it('rejects malformed UTF-16 path input before URL encoding', async () => { + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await expect(client.request(getJob, { pathParams: { jobId: '\uD800' } })).rejects.toMatchObject( + { category: 'invalid_input' } + ) + expect(mockValidateUrl).not.toHaveBeenCalled() + }) + it('suppresses arbitrary provider bodies in failed requests', async () => { mockSecureFetch.mockResolvedValue( secureResponse({ @@ -200,6 +211,39 @@ describe('Oracle EPM guarded client', () => { expect(mockSecureFetch).not.toHaveBeenCalled() }) + it('rejects accessors and inherited JSON serializers without invoking them', async () => { + const endpoint = routes.defineEndpoint({ + method: 'POST', + version: 'v3', + path: [oracleEpmLiteral('jobs')], + body: 'json', + maxRequestBytes: 1_024, + response: 'json', + timeoutMs: 2_000, + maxResponseBytes: 1_024, + }) + const inheritedSerializer = vi.fn(() => ({ changed: true })) + const inherited = Object.create({ toJSON: inheritedSerializer }) as Record + inherited.value = 'safe' + const getter = vi.fn(() => 'secret') + const accessor = {} + Object.defineProperty(accessor, 'value', { enumerable: true, get: getter }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + + await expect(client.request(endpoint, { json: inherited })).rejects.toMatchObject({ + category: 'invalid_input', + }) + await expect(client.request(endpoint, { json: accessor })).rejects.toMatchObject({ + category: 'invalid_input', + }) + expect(inheritedSerializer).not.toHaveBeenCalled() + expect(getter).not.toHaveBeenCalled() + expect(mockValidateUrl).not.toHaveBeenCalled() + }) + it('propagates caller aborts before opening a pinned request', async () => { const controller = new AbortController() controller.abort(new DOMException('user', 'AbortError')) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts index dafa2e64553..0f8aba231af 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -36,6 +36,7 @@ import type { } from '@/lib/internal/oracle-epm/types' const SAFE_TOKEN = /^[A-Za-z0-9+/]+={0,2}$/ +const LONE_SURROGATE = /[\uD800-\uDFFF]/ const validatedLinks = new WeakMap< object, { owner: object; url: string; policy: OracleEpmReturnedLinkPolicyDefinition } @@ -63,6 +64,7 @@ function validatePathValue( value === '.' || value === '..' || /[/\\\u0000-\u001f\u007f]/.test(value) || + LONE_SURROGATE.test(value) || Buffer.byteLength(value, 'utf8') > declaration.maxBytes || (declaration.pattern && !declaration.pattern.test(value)) ) { @@ -91,6 +93,7 @@ function serializeQueryValue(value: unknown, declaration: OracleEpmQueryParamete if (declaration.kind === 'string') { if ( typeof value !== 'string' || + LONE_SURROGATE.test(value) || Buffer.byteLength(value, 'utf8') > declaration.maxBytes || (declaration.pattern && !declaration.pattern.test(value)) ) @@ -149,6 +152,7 @@ function buildHeaders( } if ( /\r|\n|\u0000/.test(value) || + LONE_SURROGATE.test(value) || Buffer.byteLength(value, 'utf8') > declaration.maxBytes || (declaration.pattern && !declaration.pattern.test(value)) ) @@ -161,6 +165,77 @@ function buildHeaders( return headers } +type JsonData = null | boolean | number | string | JsonData[] | { [key: string]: JsonData } + +function hasPrototypeToJson(value: object): boolean { + let prototype = Object.getPrototypeOf(value) + while (prototype) { + if (Object.getOwnPropertyDescriptor(prototype, 'toJSON')) return true + prototype = Object.getPrototypeOf(prototype) + } + return false +} + +/** Copies only ordinary JSON data without invoking accessors or custom serializers. */ +function cloneJsonData(value: unknown, ancestors = new WeakSet()): JsonData { + if ( + value === null || + typeof value === 'string' || + typeof value === 'boolean' || + (typeof value === 'number' && Number.isFinite(value)) + ) { + return value + } + if (typeof value !== 'object') throw oracleEpmLocalError('invalid_input') + if (ancestors.has(value)) throw oracleEpmLocalError('invalid_input') + + const prototype = Object.getPrototypeOf(value) + if (Array.isArray(value)) { + if (prototype !== Array.prototype || hasPrototypeToJson(value)) { + throw oracleEpmLocalError('invalid_input') + } + const ownKeys = Reflect.ownKeys(value) + if ( + ownKeys.some( + (key) => typeof key !== 'string' || (key !== 'length' && !/^(0|[1-9][0-9]*)$/.test(key)) + ) + ) { + throw oracleEpmLocalError('invalid_input') + } + ancestors.add(value) + try { + return Array.from({ length: value.length }, (_, index) => { + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)) + if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) { + throw oracleEpmLocalError('invalid_input') + } + return cloneJsonData(descriptor.value, ancestors) + }) + } finally { + ancestors.delete(value) + } + } + + if (prototype !== Object.prototype && prototype !== null) { + throw oracleEpmLocalError('invalid_input') + } + const clone: { [key: string]: JsonData } = Object.create(null) + ancestors.add(value) + try { + for (const key of Reflect.ownKeys(value)) { + if (typeof key !== 'string') throw oracleEpmLocalError('invalid_input') + const descriptor = Object.getOwnPropertyDescriptor(value, key) + if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) { + throw oracleEpmLocalError('invalid_input') + } + clone[key] = cloneJsonData(descriptor.value, ancestors) + } + } finally { + ancestors.delete(value) + } + return clone +} + function buildBody( endpoint: OracleEpmEndpointDefinition, input: OracleEpmRequestInput @@ -175,7 +250,7 @@ function buildBody( throw oracleEpmLocalError('invalid_input') let body: string try { - body = JSON.stringify(input.json) + body = JSON.stringify(cloneJsonData(input.json)) } catch { throw oracleEpmLocalError('invalid_input') } @@ -308,7 +383,8 @@ export function createOracleEpmClient(input: { } if (deadlineSignal.aborted) throw oracleEpmLocalError('timeout', true) if (!validation.isValid) throw oracleEpmLocalError('invalid_configuration') - const maxAttempts = endpoint.retry?.maxAttempts ?? 1 + const maxAttempts = + endpoint.method === 'GET' ? Math.min(endpoint.retry?.maxAttempts ?? 1, 2) : 1 for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { let response: SecureFetchResponse try { diff --git a/apps/sim/lib/internal/oracle-epm/destination.test.ts b/apps/sim/lib/internal/oracle-epm/destination.test.ts index a418628fc15..8f92327b7f7 100644 --- a/apps/sim/lib/internal/oracle-epm/destination.test.ts +++ b/apps/sim/lib/internal/oracle-epm/destination.test.ts @@ -20,13 +20,23 @@ describe('Oracle EPM destination', () => { }) }) + it('round-trips canonical percent-encoding in a credential-owned base path', () => { + const normalized = normalizeOracleEpmDestination( + 'https://epm.example.com/gateway/My Folder/日本' + ) + expect(normalized).toBe('https://epm.example.com/gateway/My%20Folder/%E6%97%A5%E6%9C%AC') + expect(normalizeOracleEpmDestination(normalized)).toBe(normalized) + }) + it.each([ 'http://epm.example.com', 'https://user@epm.example.com', 'https://epm.example.com?token=secret', 'https://epm.example.com/#fragment', 'https://epm.example.com/%2e%2e/admin', + 'https://epm.example.com/%252e%252e/admin', 'https://epm.example.com/a%2Fb', + 'https:////epm.example.com/gateway', 'https://epm.example.com/a\\b', ])('rejects unsafe destination %j', (value) => { expect(() => defineOracleEpmDestination(value)).toThrow() diff --git a/apps/sim/lib/internal/oracle-epm/destination.ts b/apps/sim/lib/internal/oracle-epm/destination.ts index ccb51ec3e29..77eb877b0e5 100644 --- a/apps/sim/lib/internal/oracle-epm/destination.ts +++ b/apps/sim/lib/internal/oracle-epm/destination.ts @@ -14,15 +14,36 @@ function decodeSegment(segment: string): string { } } +function rawPathSegments(value: string): string[] { + const match = /^https:\/\/[^/?#]+(\/[^?#]*)?(?:[?#].*)?$/i.exec(value) + if (!match) throw new Error('Oracle EPM environment URL is invalid') + return (match[1] ?? '').split('/').filter(Boolean) +} + +function validateAndDecodeSegment(encoded: string): string { + const decoded = decodeSegment(encoded) + let safetyValue = decoded + for (let depth = 0; depth < 4 && /%[0-9A-Fa-f]{2}/.test(safetyValue); depth += 1) { + safetyValue = decodeSegment(safetyValue) + } + if ( + !decoded || + /%[0-9A-Fa-f]{2}/.test(safetyValue) || + safetyValue === '.' || + safetyValue === '..' || + safetyValue.includes('/') || + FORBIDDEN_TEXT.test(safetyValue) || + Buffer.byteLength(decoded, 'utf8') > MAX_PATH_SEGMENT_BYTES + ) { + throw new Error('Oracle EPM environment URL base path is invalid') + } + return decoded +} + /** Validates and freezes the credential-bound Oracle EPM environment URL. */ export function defineOracleEpmDestination(rawUrl: string): OracleEpmDestination { const value = rawUrl.trim() - if ( - !value || - value.length > MAX_DESTINATION_LENGTH || - value.includes('%') || - FORBIDDEN_TEXT.test(value) - ) { + if (!value || value.length > MAX_DESTINATION_LENGTH || FORBIDDEN_TEXT.test(value)) { throw new Error('Oracle EPM environment URL is invalid') } @@ -44,24 +65,11 @@ export function defineOracleEpmDestination(rawUrl: string): OracleEpmDestination ) } - const encodedSegments = parsed.pathname.split('/').filter(Boolean) + const encodedSegments = rawPathSegments(value) if (encodedSegments.length > MAX_PATH_SEGMENTS) { throw new Error('Oracle EPM environment URL base path has too many segments') } - const baseSegments = encodedSegments.map((encoded) => { - const decoded = decodeSegment(encoded) - if ( - !decoded || - decoded === '.' || - decoded === '..' || - decoded.includes('/') || - FORBIDDEN_TEXT.test(decoded) || - Buffer.byteLength(decoded, 'utf8') > MAX_PATH_SEGMENT_BYTES - ) { - throw new Error('Oracle EPM environment URL base path is invalid') - } - return decoded - }) + const baseSegments = encodedSegments.map(validateAndDecodeSegment) const destination = Object.freeze({}) as OracleEpmDestination destinations.set(destination, { diff --git a/apps/sim/lib/internal/oracle-epm/endpoint.test.ts b/apps/sim/lib/internal/oracle-epm/endpoint.test.ts index 2fd50a4919c..efa4c700395 100644 --- a/apps/sim/lib/internal/oracle-epm/endpoint.test.ts +++ b/apps/sim/lib/internal/oracle-epm/endpoint.test.ts @@ -58,6 +58,21 @@ describe('Oracle EPM endpoints', () => { maxResponseBytes: 100, }) ).toThrow('header') + + for (const correlationHeader of ['Authorization', 'Set-Cookie', 'WWW-Authenticate']) { + expect(() => + routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [], + body: 'none', + response: 'json', + timeoutMs: 1_000, + maxResponseBytes: 100, + errors: { correlationHeaders: [correlationHeader] }, + }) + ).toThrow('error policy') + } }) it('requires bounded request bodies and safe retry policies', () => { @@ -85,6 +100,31 @@ describe('Oracle EPM endpoints', () => { retry: { maxAttempts: 2, statuses: [503], initialDelayMs: 1, maxDelayMs: 2 }, }) ).toThrow('retry policy') + expect(() => + routes.defineEndpoint({ + method: 'PUT', + version: 'v3', + path: [], + body: 'json', + maxRequestBytes: 100, + response: 'json', + timeoutMs: 1_000, + maxResponseBytes: 100, + retry: { maxAttempts: 2, statuses: [503], initialDelayMs: 1, maxDelayMs: 2 }, + }) + ).toThrow('retry policy') + expect(() => + routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [], + body: 'none', + response: 'json', + timeoutMs: 1_000, + maxResponseBytes: 100, + retry: { maxAttempts: 3, statuses: [503], initialDelayMs: 1, maxDelayMs: 2 }, + }) + ).toThrow('retry policy') }) it('rejects forged endpoints', () => { diff --git a/apps/sim/lib/internal/oracle-epm/endpoint.ts b/apps/sim/lib/internal/oracle-epm/endpoint.ts index 8db7a5a1791..a236bf48a78 100644 --- a/apps/sim/lib/internal/oracle-epm/endpoint.ts +++ b/apps/sim/lib/internal/oracle-epm/endpoint.ts @@ -22,6 +22,13 @@ const FORBIDDEN_HEADERS = new Set([ 'set-cookie', 'transfer-encoding', ]) +const FORBIDDEN_CORRELATION_HEADERS = new Set([ + ...FORBIDDEN_HEADERS, + 'authentication-info', + 'proxy-authenticate', + 'proxy-authentication-info', + 'www-authenticate', +]) const TOKEN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/ const endpoints = new WeakMap() @@ -47,7 +54,7 @@ function validatePath(path: readonly OracleEpmPathPart[]): void { !part.value || part.value === '.' || part.value === '..' || - /[/\\\u0000-\u001f\u007f]/.test(part.value) || + /[/\\\u0000-\u001f\u007f\uD800-\uDFFF]/.test(part.value) || Buffer.byteLength(part.value, 'utf8') > MAX_LITERAL_BYTES ) { throw new Error('Oracle EPM endpoint literal path segment is invalid') @@ -231,10 +238,10 @@ export function defineOracleEpmEndpoint( if (declaration.retry) { const { maxAttempts, statuses, initialDelayMs, maxDelayMs } = declaration.retry if ( - !['GET', 'HEAD', 'PUT', 'DELETE'].includes(declaration.method) || + declaration.method !== 'GET' || !Number.isInteger(maxAttempts) || maxAttempts < 1 || - maxAttempts > 5 || + maxAttempts > 2 || !statuses.length || statuses.some((status) => !Number.isInteger(status) || status < 400 || status > 599) || !Number.isInteger(initialDelayMs) || @@ -252,7 +259,10 @@ export function defineOracleEpmEndpoint( declaration.errors?.providerCodePath?.some((part) => !TOKEN.test(part)) || allowedProviderCodes.some((code) => !code || code.length > 128) || new Set(allowedProviderCodes).size !== allowedProviderCodes.length || - correlationHeaders.some((name) => !/^[A-Za-z0-9-]+$/.test(name)) || + correlationHeaders.some( + (name) => + !/^[A-Za-z0-9-]+$/.test(name) || FORBIDDEN_CORRELATION_HEADERS.has(name.toLowerCase()) + ) || new Set(correlationHeaders.map((name) => name.toLowerCase())).size !== correlationHeaders.length ) { throw new Error('Oracle EPM endpoint error policy is invalid') diff --git a/apps/sim/lib/internal/oracle-epm/files.server.test.ts b/apps/sim/lib/internal/oracle-epm/files.server.test.ts index ffd4cef5acd..63ba7a846ed 100644 --- a/apps/sim/lib/internal/oracle-epm/files.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/files.server.test.ts @@ -255,4 +255,66 @@ describe('Oracle EPM file primitives', () => { context: 'execution', }) }) + + it('serializes cancellation cleanup after multipart completion settles', async () => { + let finishCompletion: ((value: { key: string; size: number }) => void) | undefined + mocks.complete.mockReturnValue( + new Promise((resolve) => { + finishCompletion = resolve + }) + ) + const controller = new AbortController() + const pending = storeOracleEpmDownload({ + body: new ReadableStream({ + start(streamController) { + streamController.close() + }, + }), + fileName: 'report.csv', + context, + maxBytes: 3, + signal: controller.signal, + }) + await vi.waitFor(() => expect(mocks.complete).toHaveBeenCalled()) + + controller.abort(new DOMException('user', 'AbortError')) + expect(mocks.abort).not.toHaveBeenCalled() + expect(mocks.deleteFile).not.toHaveBeenCalled() + finishCompletion?.({ key: 'execution/key/report.csv', size: 0 }) + + await expect(pending).rejects.toMatchObject({ name: 'AbortError' }) + expect(mocks.abort).not.toHaveBeenCalled() + expect(mocks.deleteFile).toHaveBeenCalledTimes(1) + expect(mocks.generatePresignedDownloadUrl).not.toHaveBeenCalled() + }) + + it('deletes completed storage when cancellation overlaps presigning', async () => { + mocks.complete.mockResolvedValue({ key: 'execution/key/report.csv', size: 0 }) + let finishPresign: ((value: string) => void) | undefined + mocks.generatePresignedDownloadUrl.mockReturnValue( + new Promise((resolve) => { + finishPresign = resolve + }) + ) + const controller = new AbortController() + const pending = storeOracleEpmDownload({ + body: new ReadableStream({ + start(streamController) { + streamController.close() + }, + }), + fileName: 'report.csv', + context, + maxBytes: 3, + signal: controller.signal, + }) + await vi.waitFor(() => expect(mocks.generatePresignedDownloadUrl).toHaveBeenCalled()) + + controller.abort(new DOMException('user', 'AbortError')) + expect(mocks.deleteFile).not.toHaveBeenCalled() + finishPresign?.('https://storage.example/signed') + + await expect(pending).rejects.toMatchObject({ name: 'AbortError' }) + expect(mocks.deleteFile).toHaveBeenCalledTimes(1) + }) }) diff --git a/apps/sim/lib/internal/oracle-epm/files.server.ts b/apps/sim/lib/internal/oracle-epm/files.server.ts index f0d90b2b1c4..81fa392f965 100644 --- a/apps/sim/lib/internal/oracle-epm/files.server.ts +++ b/apps/sim/lib/internal/oracle-epm/files.server.ts @@ -127,28 +127,32 @@ export async function storeOracleEpmDownload(input: { const fileName = safeFileName(input.fileName) const contentType = safeContentType(input.contentType) const key = generateUniqueExecutionFileKey(input.context, fileName) + input.signal?.throwIfAborted() const upload = await createMultipartUpload({ key, context: 'execution', contentType, completionPolicy: 'create-only', }) - const reader = input.body.getReader() + let reader: ReadableStreamDefaultReader + try { + reader = input.body.getReader() + } catch (error) { + await upload.abort().catch(() => undefined) + throw error + } let bytes = 0 let completed = false - let cleanupPromise: Promise | undefined - const cleanup = (): Promise => { - cleanupPromise ??= completed + const cleanup = (): Promise => + completed ? deleteFile({ key, context: 'execution' }).catch(() => undefined) : upload.abort().catch(() => undefined) - return cleanupPromise - } const abort = () => { void reader.cancel(input.signal?.reason).catch(() => undefined) - void cleanup() } - input.signal?.addEventListener('abort', abort, { once: true }) try { + input.signal?.throwIfAborted() + input.signal?.addEventListener('abort', abort, { once: true }) while (true) { input.signal?.throwIfAborted() const { done, value } = await reader.read() @@ -163,8 +167,10 @@ export async function storeOracleEpmDownload(input: { } await upload.write(Buffer.from(value)) } + input.signal?.throwIfAborted() const stored = await upload.complete() completed = true + input.signal?.throwIfAborted() if (stored.size !== bytes) { throw new Error('Oracle EPM download storage size did not match the streamed bytes') } @@ -173,6 +179,7 @@ export async function storeOracleEpmDownload(input: { 'execution', EXECUTION_DOWNLOAD_URL_TTL_SECONDS ) + input.signal?.throwIfAborted() return { id: generateFileId(), name: fileName, @@ -188,7 +195,6 @@ export async function storeOracleEpmDownload(input: { throw error } finally { input.signal?.removeEventListener('abort', abort) - if (!completed) await cleanup() reader.releaseLock() } } diff --git a/apps/sim/lib/internal/oracle-epm/jobs.test.ts b/apps/sim/lib/internal/oracle-epm/jobs.test.ts index 8e8aae188f4..849eba12f55 100644 --- a/apps/sim/lib/internal/oracle-epm/jobs.test.ts +++ b/apps/sim/lib/internal/oracle-epm/jobs.test.ts @@ -91,4 +91,44 @@ describe('pollOracleEpmJob', () => { }) ).rejects.toMatchObject({ name: 'TimeoutError' }) }) + + it('enforces the scheduler deadline when a child read ignores cancellation', async () => { + let readSignal: AbortSignal | undefined + const startedAt = Date.now() + await expect( + pollOracleEpmJob({ + read: async (signal) => { + readSignal = signal + return new Promise(() => undefined) + }, + classify: () => ({ state: 'pending' }), + maxWaitMs: 40, + cleanupReserveMs: 10, + maxAttempts: 2, + initialDelayMs: 1, + maxDelayMs: 1, + }) + ).rejects.toMatchObject({ name: 'TimeoutError' }) + expect(Date.now() - startedAt).toBeLessThan(500) + expect(readSignal?.aborted).toBe(true) + }) + + it('does not return a terminal result when cancellation occurs during classification', async () => { + const controller = new AbortController() + await expect( + pollOracleEpmJob({ + read: async () => ({ status: 'DONE' }), + classify: () => { + controller.abort(new DOMException('user', 'AbortError')) + return { state: 'success', result: 42 } + }, + signal: controller.signal, + maxWaitMs: 1_000, + cleanupReserveMs: 10, + maxAttempts: 1, + initialDelayMs: 1, + maxDelayMs: 1, + }) + ).rejects.toMatchObject({ name: 'AbortError' }) + }) }) diff --git a/apps/sim/lib/internal/oracle-epm/jobs.ts b/apps/sim/lib/internal/oracle-epm/jobs.ts index 61add7331e4..392dfa43f8d 100644 --- a/apps/sim/lib/internal/oracle-epm/jobs.ts +++ b/apps/sim/lib/internal/oracle-epm/jobs.ts @@ -65,6 +65,45 @@ function assertClassification( throw new Error('Oracle EPM polling classifier returned an invalid state') } +function throwIfPollingEnded(signal: AbortSignal, deadline: number): void { + if (signal.aborted) { + throw signal.reason ?? new DOMException('Oracle EPM polling aborted', 'AbortError') + } + if (Date.now() >= deadline) { + throw new DOMException('Oracle EPM polling deadline exceeded', 'TimeoutError') + } +} + +function runAbortable(operation: () => Promise, signal: AbortSignal): Promise { + if (signal.aborted) { + return Promise.reject( + signal.reason ?? new DOMException('Oracle EPM polling aborted', 'AbortError') + ) + } + return new Promise((resolve, reject) => { + let settled = false + const finish = (callback: () => void) => { + if (settled) return + settled = true + signal.removeEventListener('abort', onAbort) + callback() + } + const onAbort = () => + finish(() => + reject(signal.reason ?? new DOMException('Oracle EPM polling aborted', 'AbortError')) + ) + signal.addEventListener('abort', onAbort, { once: true }) + try { + operation().then( + (value) => finish(() => resolve(value)), + (error: unknown) => finish(() => reject(error)) + ) + } catch (error) { + finish(() => reject(error)) + } + }) +} + /** * Runs child-supplied polling semantics inside caller and execution deadlines. * The scheduler knows nothing about Oracle job ids, statuses, results, or cancellation. @@ -90,14 +129,12 @@ export async function pollOracleEpmJob( const signal = options.signal ? AbortSignal.any([options.signal, deadlineSignal]) : deadlineSignal for (let attempt = 1; attempt <= options.maxAttempts; attempt += 1) { - if (signal.aborted || Date.now() >= deadline) { - throw ( - signal.reason ?? new DOMException('Oracle EPM polling deadline exceeded', 'TimeoutError') - ) - } - const snapshot = await options.read(signal) + throwIfPollingEnded(signal, deadline) + const snapshot = await runAbortable(() => options.read(signal), signal) + throwIfPollingEnded(signal, deadline) const classification: unknown = options.classify(snapshot) assertClassification(classification) + throwIfPollingEnded(signal, deadline) if (classification.state === 'success') { return Object.freeze({ state: 'success' as const, diff --git a/apps/sim/lib/oauth/token-resolution.test.ts b/apps/sim/lib/oauth/token-resolution.test.ts index 99aa1a3d9ac..d598e20bf43 100644 --- a/apps/sim/lib/oauth/token-resolution.test.ts +++ b/apps/sim/lib/oauth/token-resolution.test.ts @@ -8,7 +8,7 @@ const { mockCaptureServerEvent, mockExecuteManagedToken, mockGetCredential, - mockGetServiceConfigByServiceId, + mockGetServiceConfigByProviderId, mockGetToolMetadata, mockRecordAudit, mockRefreshTokenIfNeeded, @@ -19,7 +19,7 @@ const { mockCaptureServerEvent: vi.fn(), mockExecuteManagedToken: vi.fn(), mockGetCredential: vi.fn(), - mockGetServiceConfigByServiceId: vi.fn(), + mockGetServiceConfigByProviderId: vi.fn(), mockGetToolMetadata: vi.fn(), mockRecordAudit: vi.fn(), mockRefreshTokenIfNeeded: vi.fn(), @@ -87,7 +87,7 @@ vi.mock('@/lib/oauth/utils', () => ({ credentialProviderId === service.providerId || credentialProviderId === service.serviceAccountProviderId, getCanonicalScopesForProvider: vi.fn().mockReturnValue([]), - getServiceConfigByServiceId: mockGetServiceConfigByServiceId, + getServiceConfigByProviderId: mockGetServiceConfigByProviderId, })) import { OrchestrationError } from '@/lib/core/orchestration/types' @@ -360,7 +360,7 @@ describe('resolveCredentialAccessToken', () => { mockGetToolMetadata.mockReturnValue({ oauth: { required: true, provider: 'google', requiredScopes: ['scope-a'] }, }) - mockGetServiceConfigByServiceId.mockReturnValue({ + mockGetServiceConfigByProviderId.mockReturnValue({ providerId: 'google', serviceAccountProviderId: 'google-service-account', }) @@ -446,6 +446,7 @@ describe('resolveCredentialAccessToken', () => { code: 'CREDENTIAL_PROVIDER_MISMATCH', error: 'Credential does not match the tool service', }) + expect(mockGetServiceConfigByProviderId).toHaveBeenCalledWith('google') expect(authenticate).not.toHaveBeenCalled() expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() }) @@ -458,7 +459,7 @@ describe('resolveCredentialAccessToken', () => { providerId: 'oracle-epm-service-account', usedCredentialTable: true, }) - mockGetServiceConfigByServiceId.mockReturnValue({ + mockGetServiceConfigByProviderId.mockReturnValue({ providerId: 'synthetic-oracle-child', serviceAccountProviderId: 'oracle-epm-service-account', }) diff --git a/apps/sim/lib/oauth/token-resolution.ts b/apps/sim/lib/oauth/token-resolution.ts index 30788aee9bf..0fe11005483 100644 --- a/apps/sim/lib/oauth/token-resolution.ts +++ b/apps/sim/lib/oauth/token-resolution.ts @@ -27,7 +27,7 @@ import { extractSalesforceInstanceUrl, isSalesforceOAuthProviderId } from '@/lib import { credentialProviderMatchesService, getCanonicalScopesForProvider, - getServiceConfigByServiceId, + getServiceConfigByProviderId, type ServiceProviderIdentity, } from '@/lib/oauth/utils' import { captureServerEvent } from '@/lib/posthog/server' @@ -376,7 +376,7 @@ export async function resolveCredentialAccessToken( const resolved = credentialId ? await resolveOAuthAccountId(credentialId) : null const toolMetadata = toolId ? getToolMetadata(toolId) : undefined const expectedService = toolMetadata?.oauth?.required - ? getServiceConfigByServiceId(toolMetadata.oauth.provider) + ? getServiceConfigByProviderId(toolMetadata.oauth.provider) : null if (credentialId && toolId && resolved?.credentialType !== 'managed_oauth' && !expectedService) { From c000b7b3cd7b324ba89a5432ba6f2ee0467a11a3 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 3 Sep 2026 19:09:27 -0700 Subject: [PATCH 03/20] fix(oracle-epm): address follow-up review findings --- .../minters/oracle-epm.test.ts | 11 +++ .../minters/oracle-epm.ts | 3 + .../internal/oracle-epm/client.server.test.ts | 49 ++++++++++++- .../lib/internal/oracle-epm/client.server.ts | 16 +++-- .../internal/oracle-epm/destination.test.ts | 2 + .../lib/internal/oracle-epm/destination.ts | 9 ++- .../internal/oracle-epm/files.server.test.ts | 68 +++++++++++++++++++ .../lib/internal/oracle-epm/files.server.ts | 2 + apps/sim/lib/oauth/token-resolution.test.ts | 46 +++++++++++++ apps/sim/lib/oauth/token-resolution.ts | 21 ++---- 10 files changed, 204 insertions(+), 23 deletions(-) diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.test.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.test.ts index 38f3ae3dd91..cceb1cf5e70 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.test.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.test.ts @@ -33,6 +33,8 @@ describe('mintOracleEpmServiceAccountToken', () => { { clientId: 'user:name', clientSecret: 'password' }, { clientId: 'user\nname', clientSecret: 'password' }, { clientId: 'user', clientSecret: 'pass\nword' }, + { clientId: 'user\uD800', clientSecret: 'password' }, + { clientId: 'user', clientSecret: 'password\uDC00' }, { clientId: '', clientSecret: 'password' }, ])('rejects unsafe Basic credential text', async (credentials) => { await expect( @@ -43,6 +45,15 @@ describe('mintOracleEpmServiceAccountToken', () => { ).rejects.toBeInstanceOf(TokenServiceAccountValidationError) }) + it('preserves valid surrogate pairs in Basic credential values', async () => { + const result = await mintOracleEpmServiceAccountToken({ + orgId: 'https://epm.example.com', + clientId: 'integration-😀', + clientSecret: 'password-🔒', + }) + expect(Buffer.from(result.accessToken, 'base64').toString()).toBe('integration-😀:password-🔒') + }) + it('does not reflect secrets in validation errors', async () => { const secret = 'password-with-newline\n' const error = await mintOracleEpmServiceAccountToken({ diff --git a/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.ts b/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.ts index fce8eb4529e..5822724ff7a 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/minters/oracle-epm.ts @@ -13,6 +13,7 @@ const SYNTHETIC_TOKEN_TTL_SECONDS = 600 const MAX_USERNAME_BYTES = 255 const MAX_AUTH_VALUE_BYTES = 1_024 const FORBIDDEN_CREDENTIAL_TEXT = /[\u0000-\u001f\u007f]/ +const MALFORMED_UTF16 = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? MAX_USERNAME_BYTES ) { throw invalidCredentials('integration username is invalid') @@ -56,6 +58,7 @@ export async function mintOracleEpmServiceAccountToken( if ( !password || FORBIDDEN_CREDENTIAL_TEXT.test(password) || + MALFORMED_UTF16.test(password) || Buffer.byteLength(password, 'utf8') > MAX_AUTH_VALUE_BYTES ) { throw invalidCredentials('password is invalid') diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index d91b181f9a7..c8a82de1760 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -1,5 +1,6 @@ /** @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' const { mockSecureFetch, mockValidateUrl } = vi.hoisted(() => ({ mockSecureFetch: vi.fn(), @@ -93,7 +94,7 @@ describe('Oracle EPM guarded client', () => { { logDetails: false } ) expect(mockSecureFetch).toHaveBeenCalledWith( - expect.any(String), + 'https://epm.example.com/gateway/acme/SyntheticAlpha/rest/v3/jobs/job%20with%20spaces?limit=25', '203.0.113.10', expect.objectContaining({ method: 'GET', @@ -146,6 +147,50 @@ describe('Oracle EPM guarded client', () => { expect(mockValidateUrl).not.toHaveBeenCalled() }) + it('preserves valid surrogate pairs in encoded path and query parameters', async () => { + const endpoint = routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], + query: { label: oracleEpmQuery.string({ maxBytes: 32 }) }, + body: 'none', + response: 'json', + timeoutMs: 2_000, + maxResponseBytes: 1_024, + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await client.request(endpoint, { + pathParams: { fileId: 'report-😀' }, + query: { label: 'locked-🔒' }, + }) + expect(mockSecureFetch.mock.calls[0][0]).toBe( + 'https://epm.example.com/SyntheticAlpha/rest/v3/files/report-%F0%9F%98%80?label=locked-%F0%9F%94%92' + ) + }) + + it('preserves streamed response size failures as payload-too-large errors', async () => { + mockSecureFetch.mockResolvedValue({ + ...secureResponse({}), + json: vi.fn().mockRejectedValue( + new PayloadSizeLimitError({ + label: 'secure fetch response', + maxBytes: 4_096, + observedBytes: 4_097, + }) + ), + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await expect(client.request(getJob, { pathParams: { jobId: '42' } })).rejects.toMatchObject({ + category: 'payload_too_large', + }) + }) + it('suppresses arbitrary provider bodies in failed requests', async () => { mockSecureFetch.mockResolvedValue( secureResponse({ @@ -310,6 +355,8 @@ describe('Oracle EPM guarded client', () => { 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x&token=y', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?unknown=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#fragment', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/ab\nc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/\uD800?token=x', ])('rejects unsafe returned link %j', (href) => { const policy = routes.defineReturnedLinkPolicy({ relation: 'download', diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts index 0f8aba231af..82c79896171 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -36,7 +36,8 @@ import type { } from '@/lib/internal/oracle-epm/types' const SAFE_TOKEN = /^[A-Za-z0-9+/]+={0,2}$/ -const LONE_SURROGATE = /[\uD800-\uDFFF]/ +const MALFORMED_UTF16 = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? declaration.maxBytes || (declaration.pattern && !declaration.pattern.test(value)) ) { @@ -93,7 +94,7 @@ function serializeQueryValue(value: unknown, declaration: OracleEpmQueryParamete if (declaration.kind === 'string') { if ( typeof value !== 'string' || - LONE_SURROGATE.test(value) || + MALFORMED_UTF16.test(value) || Buffer.byteLength(value, 'utf8') > declaration.maxBytes || (declaration.pattern && !declaration.pattern.test(value)) ) @@ -152,7 +153,7 @@ function buildHeaders( } if ( /\r|\n|\u0000/.test(value) || - LONE_SURROGATE.test(value) || + MALFORMED_UTF16.test(value) || Buffer.byteLength(value, 'utf8') > declaration.maxBytes || (declaration.pattern && !declaration.pattern.test(value)) ) @@ -305,7 +306,8 @@ async function projectResponse( try { const data = await response.json() return Object.freeze({ status: response.status, data, correlationId }) - } catch { + } catch (error) { + if (isPayloadSizeLimitError(error)) throw oracleEpmLocalError('payload_too_large') throw oracleEpmLocalError('invalid_response') } } @@ -464,7 +466,9 @@ export function createOracleEpmClient(input: { link.rel !== policy.relation || (link.method !== undefined && link.method !== policy.method) || typeof link.href !== 'string' || - link.href.length > 8_192 + link.href.length > 8_192 || + FORBIDDEN_LINK_TEXT.test(link.href) || + MALFORMED_UTF16.test(link.href) ) throw oracleEpmLocalError('invalid_input') let url: URL diff --git a/apps/sim/lib/internal/oracle-epm/destination.test.ts b/apps/sim/lib/internal/oracle-epm/destination.test.ts index 8f92327b7f7..16c9cd0778c 100644 --- a/apps/sim/lib/internal/oracle-epm/destination.test.ts +++ b/apps/sim/lib/internal/oracle-epm/destination.test.ts @@ -38,6 +38,8 @@ describe('Oracle EPM destination', () => { 'https://epm.example.com/a%2Fb', 'https:////epm.example.com/gateway', 'https://epm.example.com/a\\b', + 'https://epm.example.com/gateway/\uD800', + 'https://epm.example.com/gateway/\uDC00', ])('rejects unsafe destination %j', (value) => { expect(() => defineOracleEpmDestination(value)).toThrow() }) diff --git a/apps/sim/lib/internal/oracle-epm/destination.ts b/apps/sim/lib/internal/oracle-epm/destination.ts index 77eb877b0e5..932f162911c 100644 --- a/apps/sim/lib/internal/oracle-epm/destination.ts +++ b/apps/sim/lib/internal/oracle-epm/destination.ts @@ -4,6 +4,7 @@ const MAX_DESTINATION_LENGTH = 2_048 const MAX_PATH_SEGMENTS = 32 const MAX_PATH_SEGMENT_BYTES = 255 const FORBIDDEN_TEXT = /[\u0000-\u001f\u007f\\]/ +const MALFORMED_UTF16 = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?() function decodeSegment(segment: string): string { @@ -33,6 +34,7 @@ function validateAndDecodeSegment(encoded: string): string { safetyValue === '..' || safetyValue.includes('/') || FORBIDDEN_TEXT.test(safetyValue) || + MALFORMED_UTF16.test(decoded) || Buffer.byteLength(decoded, 'utf8') > MAX_PATH_SEGMENT_BYTES ) { throw new Error('Oracle EPM environment URL base path is invalid') @@ -43,7 +45,12 @@ function validateAndDecodeSegment(encoded: string): string { /** Validates and freezes the credential-bound Oracle EPM environment URL. */ export function defineOracleEpmDestination(rawUrl: string): OracleEpmDestination { const value = rawUrl.trim() - if (!value || value.length > MAX_DESTINATION_LENGTH || FORBIDDEN_TEXT.test(value)) { + if ( + !value || + value.length > MAX_DESTINATION_LENGTH || + FORBIDDEN_TEXT.test(value) || + MALFORMED_UTF16.test(value) + ) { throw new Error('Oracle EPM environment URL is invalid') } diff --git a/apps/sim/lib/internal/oracle-epm/files.server.test.ts b/apps/sim/lib/internal/oracle-epm/files.server.test.ts index 63ba7a846ed..9368a4bae22 100644 --- a/apps/sim/lib/internal/oracle-epm/files.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/files.server.test.ts @@ -187,6 +187,74 @@ describe('Oracle EPM file primitives', () => { expect(destroy).toHaveBeenCalled() }) + it('destroys a source canceled while its storage stream is opening', async () => { + let finishOpen: ((stream: Readable) => void) | undefined + mocks.downloadFileStream.mockReturnValue( + new Promise((resolve) => { + finishOpen = resolve + }) + ) + const controller = new AbortController() + const source = await openOracleEpmSourceFile({ + file: { + id: 'f', + name: 'x', + url: '', + size: 0, + type: '', + key: 'workspace/key', + context: 'workspace', + }, + userId: 'user-1', + maxBytes: 3, + signal: controller.signal, + }) + const pending = (async () => { + for await (const _chunk of source.chunks) { + // Consume the guarded stream. + } + })() + await vi.waitFor(() => expect(mocks.downloadFileStream).toHaveBeenCalled()) + controller.abort(new DOMException('user', 'AbortError')) + const stream = Readable.from([]) + const destroy = vi.spyOn(stream, 'destroy') + finishOpen?.(stream) + + await expect(pending).rejects.toMatchObject({ name: 'AbortError' }) + expect(destroy).toHaveBeenCalled() + }) + + it('rejects a source canceled as an empty storage stream reaches EOF', async () => { + const controller = new AbortController() + const stream = new Readable({ + read() { + this.push(null) + controller.abort(new DOMException('user', 'AbortError')) + }, + }) + mocks.downloadFileStream.mockResolvedValue(stream) + const source = await openOracleEpmSourceFile({ + file: { + id: 'f', + name: 'x', + url: '', + size: 0, + type: '', + key: 'workspace/key', + context: 'workspace', + }, + userId: 'user-1', + maxBytes: 3, + signal: controller.signal, + }) + + await expect(async () => { + for await (const _chunk of source.chunks) { + // Consume the guarded stream. + } + }).rejects.toMatchObject({ name: 'AbortError' }) + }) + it('streams a bounded provider response into execution storage and returns UserFile', async () => { const body = new ReadableStream({ start(controller) { diff --git a/apps/sim/lib/internal/oracle-epm/files.server.ts b/apps/sim/lib/internal/oracle-epm/files.server.ts index 81fa392f965..379f9a09dd0 100644 --- a/apps/sim/lib/internal/oracle-epm/files.server.ts +++ b/apps/sim/lib/internal/oracle-epm/files.server.ts @@ -79,6 +79,7 @@ export async function openOracleEpmSourceFile(input: { const abort = () => stream.destroy(signal?.reason) signal?.addEventListener('abort', abort, { once: true }) try { + signal?.throwIfAborted() for await (const chunk of stream) { signal?.throwIfAborted() const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array) @@ -92,6 +93,7 @@ export async function openOracleEpmSourceFile(input: { } yield buffer } + signal?.throwIfAborted() } finally { signal?.removeEventListener('abort', abort) stream.destroy() diff --git a/apps/sim/lib/oauth/token-resolution.test.ts b/apps/sim/lib/oauth/token-resolution.test.ts index d598e20bf43..b1e30f8bd64 100644 --- a/apps/sim/lib/oauth/token-resolution.test.ts +++ b/apps/sim/lib/oauth/token-resolution.test.ts @@ -492,6 +492,52 @@ describe('resolveCredentialAccessToken', () => { }) }) + it('preserves an existing service-account tool without OAuth service metadata', async () => { + mockResolveOAuthAccountId.mockResolvedValue({ + accountId: 'credential-1', + credentialId: 'credential-1', + credentialType: 'service_account', + providerId: 'claude-platform-service-account', + usedCredentialTable: true, + }) + mockGetToolMetadata.mockReturnValue({ oauth: undefined }) + mockAuthorizeCredentialUseForAuth.mockResolvedValue({ + ok: true, + requesterUserId: 'user-1', + credentialOwnerUserId: 'owner-1', + workspaceId: 'ws-1', + resolvedCredentialId: 'credential-1', + }) + mockResolveServiceAccountToken.mockResolvedValue({ accessToken: 'workspace-api-key' }) + + await expect( + resolveCredentialAccessToken({ + requestId: 'req-1', + credentialId: 'credential-1', + toolId: 'managed_agent_run_session', + authenticate, + }) + ).resolves.toEqual({ + ok: true, + token: { + accessToken: 'workspace-api-key', + credentialType: 'service_account', + apiDomain: undefined, + authStyle: undefined, + cloudId: undefined, + domain: undefined, + instanceUrl: undefined, + }, + }) + expect(mockGetServiceConfigByProviderId).not.toHaveBeenCalled() + expect(mockResolveServiceAccountToken).toHaveBeenCalledWith( + 'credential-1', + 'claude-platform-service-account', + [], + undefined + ) + }) + it('rejects a mismatched OAuth account after loading its authoritative provider', async () => { mockResolveOAuthAccountId.mockResolvedValue({ accountId: 'account-1', diff --git a/apps/sim/lib/oauth/token-resolution.ts b/apps/sim/lib/oauth/token-resolution.ts index 0fe11005483..42abc35a843 100644 --- a/apps/sim/lib/oauth/token-resolution.ts +++ b/apps/sim/lib/oauth/token-resolution.ts @@ -379,7 +379,12 @@ export async function resolveCredentialAccessToken( ? getServiceConfigByProviderId(toolMetadata.oauth.provider) : null - if (credentialId && toolId && resolved?.credentialType !== 'managed_oauth' && !expectedService) { + if ( + resolved?.credentialType !== 'managed_oauth' && + resolved?.providerId && + expectedService && + !credentialProviderMatchesService(resolved.providerId, expectedService) + ) { return { ok: false, status: 403, @@ -388,20 +393,6 @@ export async function resolveCredentialAccessToken( } } - if (resolved?.credentialType !== 'managed_oauth' && resolved?.providerId && toolId) { - if ( - !expectedService || - !credentialProviderMatchesService(resolved.providerId, expectedService) - ) { - return { - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - } - } - } - if (resolved?.credentialType !== 'managed_oauth' || !resolved.credentialId) { const auth = await input.authenticate() return resolveCredentialToken(auth, { From 5e32a640aae2908b518692bddf97557194906b74 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 3 Sep 2026 19:24:01 -0700 Subject: [PATCH 04/20] fix(oracle-epm): validate raw returned-link paths --- .../internal/oracle-epm/client.server.test.ts | 10 +++ .../lib/internal/oracle-epm/client.server.ts | 63 ++++++++++++++----- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index c8a82de1760..2f2bbcc02a7 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -355,8 +355,18 @@ describe('Oracle EPM guarded client', () => { 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x&token=y', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?unknown=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#fragment', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/ab\nc?token=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/\uD800?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest//v3/files/abc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc/?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/./abc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e%2e?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e.?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%252e%252e?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%2Fabc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%5Cabc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files\\abc?token=x', ])('rejects unsafe returned link %j', (href) => { const policy = routes.defineReturnedLinkPolicy({ relation: 'download', diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts index 82c79896171..a73dd123093 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -312,15 +312,52 @@ async function projectResponse( } } -function matchReturnedPath(candidate: string[], expected: readonly OracleEpmPathPart[]): void { - if (candidate.length !== expected.length) throw oracleEpmLocalError('invalid_input') - for (let index = 0; index < expected.length; index += 1) { - let decoded: string +function decodeReturnedPathSegment(encoded: string): string { + let decoded: string + try { + decoded = decodeURIComponent(encoded) + } catch { + throw oracleEpmLocalError('invalid_input') + } + let safetyValue = decoded + for (let depth = 0; depth < 4 && /%[0-9A-Fa-f]{2}/.test(safetyValue); depth += 1) { try { - decoded = decodeURIComponent(candidate[index]) + safetyValue = decodeURIComponent(safetyValue) } catch { throw oracleEpmLocalError('invalid_input') } + } + if ( + !decoded || + /%[0-9A-Fa-f]{2}/.test(safetyValue) || + safetyValue === '.' || + safetyValue === '..' || + /[/\\\u0000-\u001f\u007f]/.test(safetyValue) || + MALFORMED_UTF16.test(decoded) + ) { + throw oracleEpmLocalError('invalid_input') + } + return decoded +} + +function rawReturnedPathSegments(href: string): string[] { + const match = /^https:\/\/[^/?#]*(\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i.exec(href) + if (!match) throw oracleEpmLocalError('invalid_input') + const rawPath = match[1] ?? '' + if (rawPath.includes('\\')) throw oracleEpmLocalError('invalid_input') + if (!rawPath) return [] + const segments = rawPath.slice(1).split('/') + if (segments.some((segment) => !segment)) throw oracleEpmLocalError('invalid_input') + return segments.map(decodeReturnedPathSegment) +} + +function matchReturnedPath( + candidate: readonly string[], + expected: readonly OracleEpmPathPart[] +): void { + if (candidate.length !== expected.length) throw oracleEpmLocalError('invalid_input') + for (let index = 0; index < expected.length; index += 1) { + const decoded = candidate[index] const part = expected[index] if (part.kind === 'literal') { if (decoded !== part.value) throw oracleEpmLocalError('invalid_input') @@ -468,9 +505,11 @@ export function createOracleEpmClient(input: { typeof link.href !== 'string' || link.href.length > 8_192 || FORBIDDEN_LINK_TEXT.test(link.href) || - MALFORMED_UTF16.test(link.href) + MALFORMED_UTF16.test(link.href) || + link.href.includes('#') ) throw oracleEpmLocalError('invalid_input') + const candidate = rawReturnedPathSegments(link.href) let url: URL try { url = new URL(link.href) @@ -480,17 +519,9 @@ export function createOracleEpmClient(input: { if (url.origin !== destinationData.origin || url.username || url.password || url.hash) throw oracleEpmLocalError('invalid_input') const route = getOracleEpmRouteSpace(policy.routeSpace) - const candidate = url.pathname.split('/').filter(Boolean) const prefix = [...destinationData.baseSegments, ...route.context, policy.version] - const prefixMatches = (expected: readonly string[]): boolean => { - try { - return expected.every( - (part, index) => decodeURIComponent(candidate[index] ?? '') === part - ) - } catch { - return false - } - } + const prefixMatches = (expected: readonly string[]): boolean => + expected.every((part, index) => candidate[index] === part) if (policy.preserveGatewayBasePath && !prefixMatches(prefix)) throw oracleEpmLocalError('invalid_input') const pathStart = policy.preserveGatewayBasePath ? prefix.length : route.context.length + 1 From 893d086105fcb8d4ea0249c5c6c1c00aafb81bfe Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 3 Sep 2026 19:30:15 -0700 Subject: [PATCH 05/20] fix(oracle-epm): reject unusable link policies --- .../sim/lib/internal/oracle-epm/links.test.ts | 22 +++++++++++++++ apps/sim/lib/internal/oracle-epm/links.ts | 7 +++-- apps/sim/lib/oauth/token-resolution.test.ts | 27 +++++++++++++++++++ apps/sim/lib/oauth/token-resolution.ts | 15 +++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/internal/oracle-epm/links.test.ts b/apps/sim/lib/internal/oracle-epm/links.test.ts index f8536f27fe6..2efbacb9ecd 100644 --- a/apps/sim/lib/internal/oracle-epm/links.test.ts +++ b/apps/sim/lib/internal/oracle-epm/links.test.ts @@ -57,4 +57,26 @@ describe('Oracle EPM returned-link declarations', () => { 'not a valid declaration' ) }) + + it('rejects endpoint-bound policies whose required headers cannot be supplied', () => { + const endpoint = routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('download')], + headers: { range: { name: 'Range', required: true, maxBytes: 64 } }, + body: 'none', + response: 'stream', + timeoutMs: 2_000, + maxResponseBytes: 1_024, + }) + + expect(() => + routes.defineReturnedLinkPolicy({ + relation: 'download', + method: 'GET', + endpoint, + preserveGatewayBasePath: true, + }) + ).toThrow('input contract') + }) }) diff --git a/apps/sim/lib/internal/oracle-epm/links.ts b/apps/sim/lib/internal/oracle-epm/links.ts index 4ddad7b25c3..401d5533f1e 100644 --- a/apps/sim/lib/internal/oracle-epm/links.ts +++ b/apps/sim/lib/internal/oracle-epm/links.ts @@ -57,9 +57,12 @@ export function defineOracleEpmReturnedLinkPolicy( if ( endpoint.routeSpace !== routeSpace || endpoint.method !== declaration.method || - endpoint.body !== 'none' + endpoint.body !== 'none' || + Object.values(endpoint.headers ?? {}).some((header) => header.required) ) { - throw new Error('Oracle EPM returned-link policy endpoint does not match its route or method') + throw new Error( + 'Oracle EPM returned-link policy endpoint does not match its route, method, or input contract' + ) } endpointDefinition = endpoint version = endpoint.version diff --git a/apps/sim/lib/oauth/token-resolution.test.ts b/apps/sim/lib/oauth/token-resolution.test.ts index b1e30f8bd64..8093a32168f 100644 --- a/apps/sim/lib/oauth/token-resolution.test.ts +++ b/apps/sim/lib/oauth/token-resolution.test.ts @@ -538,6 +538,33 @@ describe('resolveCredentialAccessToken', () => { ) }) + it('fails closed when declared tool service metadata has no registered service', async () => { + mockResolveOAuthAccountId.mockResolvedValue({ + accountId: 'credential-1', + credentialId: 'credential-1', + credentialType: 'service_account', + providerId: 'oracle-epm-service-account', + usedCredentialTable: true, + }) + mockGetServiceConfigByProviderId.mockReturnValue(null) + + await expect( + resolveCredentialAccessToken({ + requestId: 'req-1', + credentialId: 'credential-1', + toolId: 'synthetic_oracle_tool', + authenticate, + }) + ).resolves.toEqual({ + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + }) + expect(authenticate).not.toHaveBeenCalled() + expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() + }) + it('rejects a mismatched OAuth account after loading its authoritative provider', async () => { mockResolveOAuthAccountId.mockResolvedValue({ accountId: 'account-1', diff --git a/apps/sim/lib/oauth/token-resolution.ts b/apps/sim/lib/oauth/token-resolution.ts index 42abc35a843..f23e7d0420c 100644 --- a/apps/sim/lib/oauth/token-resolution.ts +++ b/apps/sim/lib/oauth/token-resolution.ts @@ -379,6 +379,21 @@ export async function resolveCredentialAccessToken( ? getServiceConfigByProviderId(toolMetadata.oauth.provider) : null + if ( + credentialId && + toolId && + resolved?.credentialType !== 'managed_oauth' && + toolMetadata?.oauth?.required && + !expectedService + ) { + return { + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + } + } + if ( resolved?.credentialType !== 'managed_oauth' && resolved?.providerId && From 849968a3bd963fad63967f1c56fedeec13ab9a42 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 3 Sep 2026 19:37:33 -0700 Subject: [PATCH 06/20] fix(credentials): authorize before provider matching --- apps/sim/lib/oauth/token-resolution.test.ts | 40 +++++++++++- apps/sim/lib/oauth/token-resolution.ts | 67 ++++++++------------- 2 files changed, 64 insertions(+), 43 deletions(-) diff --git a/apps/sim/lib/oauth/token-resolution.test.ts b/apps/sim/lib/oauth/token-resolution.test.ts index 8093a32168f..bcb250f540a 100644 --- a/apps/sim/lib/oauth/token-resolution.test.ts +++ b/apps/sim/lib/oauth/token-resolution.test.ts @@ -432,6 +432,13 @@ describe('resolveCredentialAccessToken', () => { providerId: 'oracle-epm-service-account', usedCredentialTable: true, }) + mockAuthorizeCredentialUseForAuth.mockResolvedValue({ + ok: true, + requesterUserId: 'user-1', + credentialOwnerUserId: 'owner-1', + workspaceId: 'ws-1', + resolvedCredentialId: 'credential-1', + }) const result = await resolveCredentialAccessToken({ requestId: 'req-1', @@ -447,7 +454,29 @@ describe('resolveCredentialAccessToken', () => { error: 'Credential does not match the tool service', }) expect(mockGetServiceConfigByProviderId).toHaveBeenCalledWith('google') - expect(authenticate).not.toHaveBeenCalled() + expect(authenticate).toHaveBeenCalledTimes(1) + expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() + }) + + it('does not reveal provider mismatches before credential authorization succeeds', async () => { + mockResolveOAuthAccountId.mockResolvedValue({ + accountId: 'credential-1', + credentialId: 'credential-1', + credentialType: 'service_account', + providerId: 'oracle-epm-service-account', + usedCredentialTable: true, + }) + mockAuthorizeCredentialUseForAuth.mockResolvedValue({ ok: false, error: 'Unauthorized' }) + + await expect( + resolveCredentialAccessToken({ + requestId: 'req-1', + credentialId: 'credential-1', + toolId: 'gmail_send', + authenticate, + }) + ).resolves.toEqual({ ok: false, status: 403, error: 'Unauthorized' }) + expect(authenticate).toHaveBeenCalledTimes(1) expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() }) @@ -547,6 +576,13 @@ describe('resolveCredentialAccessToken', () => { usedCredentialTable: true, }) mockGetServiceConfigByProviderId.mockReturnValue(null) + mockAuthorizeCredentialUseForAuth.mockResolvedValue({ + ok: true, + requesterUserId: 'user-1', + credentialOwnerUserId: 'owner-1', + workspaceId: 'ws-1', + resolvedCredentialId: 'credential-1', + }) await expect( resolveCredentialAccessToken({ @@ -561,7 +597,7 @@ describe('resolveCredentialAccessToken', () => { code: 'CREDENTIAL_PROVIDER_MISMATCH', error: 'Credential does not match the tool service', }) - expect(authenticate).not.toHaveBeenCalled() + expect(authenticate).toHaveBeenCalledTimes(1) expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() }) diff --git a/apps/sim/lib/oauth/token-resolution.ts b/apps/sim/lib/oauth/token-resolution.ts index f23e7d0420c..4e7db06de0f 100644 --- a/apps/sim/lib/oauth/token-resolution.ts +++ b/apps/sim/lib/oauth/token-resolution.ts @@ -64,8 +64,12 @@ export interface ResolveCredentialTokenInput { auditRequest?: CredentialAuditRequest /** Credential lookup already performed by {@link resolveCredentialAccessToken}'s dispatch. */ resolvedCredential: ResolvedCredential | null - /** Trusted tool-service identity used to reject cross-service credentials. */ - expectedService?: ServiceProviderIdentity + /** + * Trusted tool-service identity used to reject cross-service credentials. + * `null` means the tool declared a service that is absent from the registry; + * `undefined` means the tool declares no OAuth service constraint. + */ + expectedService?: ServiceProviderIdentity | null } export type ResolveCredentialTokenResult = @@ -213,8 +217,22 @@ export async function resolveCredentialToken( workflowId, callerUserId, }) + const isServiceAccount = + resolved?.credentialType === 'service_account' && Boolean(resolved.credentialId) - if (resolved?.credentialType === 'service_account' && resolved.credentialId) { + if (!authz.ok || (!isServiceAccount && !authz.credentialOwnerUserId)) { + return { ok: false, status: 403, error: authz.error || 'Unauthorized' } + } + if (input.expectedService === null) { + return { + ok: false, + status: 403, + code: 'CREDENTIAL_PROVIDER_MISMATCH', + error: 'Credential does not match the tool service', + } + } + + if (isServiceAccount && resolved?.credentialId) { if ( input.expectedService && (!resolved.providerId || @@ -227,9 +245,6 @@ export async function resolveCredentialToken( error: 'Credential does not match the tool service', } } - if (!authz.ok) { - return { ok: false, status: 403, error: authz.error || 'Unauthorized' } - } const saActorId = authz.requesterUserId const saWorkspaceId = resolved.workspaceId ?? authz.workspaceId ?? null @@ -303,10 +318,9 @@ export async function resolveCredentialToken( } } - if (!authz.ok || !authz.credentialOwnerUserId) { - return { ok: false, status: 403, error: authz.error || 'Unauthorized' } + if (!authz.credentialOwnerUserId) { + return { ok: false, status: 403, error: 'Unauthorized' } } - const resolvedCredentialId = authz.resolvedCredentialId || credentialId const credential = await getCredential( requestId, @@ -376,37 +390,8 @@ export async function resolveCredentialAccessToken( const resolved = credentialId ? await resolveOAuthAccountId(credentialId) : null const toolMetadata = toolId ? getToolMetadata(toolId) : undefined const expectedService = toolMetadata?.oauth?.required - ? getServiceConfigByProviderId(toolMetadata.oauth.provider) - : null - - if ( - credentialId && - toolId && - resolved?.credentialType !== 'managed_oauth' && - toolMetadata?.oauth?.required && - !expectedService - ) { - return { - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - } - } - - if ( - resolved?.credentialType !== 'managed_oauth' && - resolved?.providerId && - expectedService && - !credentialProviderMatchesService(resolved.providerId, expectedService) - ) { - return { - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - } - } + ? (getServiceConfigByProviderId(toolMetadata.oauth.provider) ?? null) + : undefined if (resolved?.credentialType !== 'managed_oauth' || !resolved.credentialId) { const auth = await input.authenticate() @@ -424,7 +409,7 @@ export async function resolveCredentialAccessToken( callerUserId: input.callerUserId, auditRequest, resolvedCredential: resolved, - expectedService: expectedService ?? undefined, + expectedService, }) } From 1a37ec6978ba39b6b6cbb9ec87b5e52f800ece76 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 09:45:10 -0700 Subject: [PATCH 07/20] refactor(oracle-epm): narrow shared infrastructure changes --- .../credentials/orchestration/index.test.ts | 40 ---- .../lib/credentials/orchestration/index.ts | 12 +- apps/sim/lib/oauth/token-resolution.test.ts | 219 ------------------ apps/sim/lib/oauth/token-resolution.ts | 76 ++---- 4 files changed, 17 insertions(+), 330 deletions(-) diff --git a/apps/sim/lib/credentials/orchestration/index.test.ts b/apps/sim/lib/credentials/orchestration/index.test.ts index c7de7da67a3..54510cd4902 100644 --- a/apps/sim/lib/credentials/orchestration/index.test.ts +++ b/apps/sim/lib/credentials/orchestration/index.test.ts @@ -286,9 +286,6 @@ describe('performUpdateCredential — service-account secret rotation', () => { it('carries the stored dataCenter forward for a client-credential reconnect', async () => { mockCredential({ providerId: 'zoho-desk-service-account', displayName: 'Acme Desk' }) mockIsClientCredentialAccountProviderId.mockReturnValue(true) - mockGetClientCredentialAccountDescriptor.mockReturnValue({ - fields: [{ id: 'dataCenter' }], - } as never) mockStoredBlob({ type: 'client_credential_account', dataCenter: 'eu' }) mockVerifyAndBuildServiceAccountSecret.mockResolvedValue({ providerId: 'zoho-desk-service-account', @@ -334,7 +331,6 @@ describe('performUpdateCredential — service-account secret rotation', () => { mockIsClientCredentialAccountProviderId.mockReturnValue(true) mockGetClientCredentialAccountDescriptor.mockReturnValue({ defaultAuthMethod: 'client_credentials', - fields: [], } as never) mockStoredBlob({ type: 'client_credential_account', @@ -418,42 +414,6 @@ describe('performUpdateCredential — service-account secret rotation', () => { ) }) - it('threads Oracle EPM integration-user fields through reconnect without reading old secrets', async () => { - mockCredential({ - providerId: 'oracle-epm-service-account', - displayName: 'Production EPM', - }) - mockIsClientCredentialAccountProviderId.mockReturnValue(true) - mockGetClientCredentialAccountDescriptor.mockReturnValue({ - fields: [{ id: 'orgId' }, { id: 'clientId' }, { id: 'clientSecret' }], - } as never) - mockStoredBlob({ type: 'client_credential_account' }) - mockVerifyAndBuildServiceAccountSecret.mockResolvedValue({ - providerId: 'oracle-epm-service-account', - encryptedServiceAccountKey: 'new-cipher', - displayName: 'Production EPM', - auditMetadata: {}, - }) - - await performUpdateCredential({ - credentialId: 'cred-1', - userId: 'user-1', - orgId: 'https://epm.example.com/gateway', - clientId: 'integration.user@example.com', - clientSecret: 'rotated-password', - }) - - expect(mockDecryptSecret).not.toHaveBeenCalled() - expect(mockVerifyAndBuildServiceAccountSecret).toHaveBeenCalledWith( - 'oracle-epm-service-account', - expect.objectContaining({ - orgId: 'https://epm.example.com/gateway', - clientId: 'integration.user@example.com', - clientSecret: 'rotated-password', - }) - ) - }) - it('surfaces a rebuild failure as a validation error and writes nothing', async () => { mockCredential() mockStoredBlob({ type: 'service_account', client_email: OLD_EMAIL }) diff --git a/apps/sim/lib/credentials/orchestration/index.ts b/apps/sim/lib/credentials/orchestration/index.ts index 19503cfbe7d..47cc51e4927 100644 --- a/apps/sim/lib/credentials/orchestration/index.ts +++ b/apps/sim/lib/credentials/orchestration/index.ts @@ -292,16 +292,12 @@ export async function updateCredentialRecord( // credential back to the US accounts server. Carry the stored value forward // when the caller did not supply one. const isClientCredentialProvider = isClientCredentialAccountProviderId(providerId) - const clientCredentialDescriptor = isClientCredentialProvider - ? getClientCredentialAccountDescriptor(providerId) - : undefined - const storesDataCenter = Boolean( - clientCredentialDescriptor?.fields.some((field) => field.id === 'dataCenter') - ) - const needsStoredDataCenter = params.dataCenter === undefined && storesDataCenter + const needsStoredDataCenter = params.dataCenter === undefined && isClientCredentialProvider // Only a multi-grant provider stores these, so single-grant ones must not // pay for a row read + decrypt that can only ever return undefined. - const isMultiGrantProvider = Boolean(clientCredentialDescriptor?.defaultAuthMethod) + const isMultiGrantProvider = Boolean( + getClientCredentialAccountDescriptor(providerId)?.defaultAuthMethod + ) const needsStoredAuthMethod = params.authMethod === undefined && isMultiGrantProvider const needsStoredUsername = params.username === undefined && isMultiGrantProvider diff --git a/apps/sim/lib/oauth/token-resolution.test.ts b/apps/sim/lib/oauth/token-resolution.test.ts index bcb250f540a..e06ee3c9c8d 100644 --- a/apps/sim/lib/oauth/token-resolution.test.ts +++ b/apps/sim/lib/oauth/token-resolution.test.ts @@ -8,7 +8,6 @@ const { mockCaptureServerEvent, mockExecuteManagedToken, mockGetCredential, - mockGetServiceConfigByProviderId, mockGetToolMetadata, mockRecordAudit, mockRefreshTokenIfNeeded, @@ -19,7 +18,6 @@ const { mockCaptureServerEvent: vi.fn(), mockExecuteManagedToken: vi.fn(), mockGetCredential: vi.fn(), - mockGetServiceConfigByProviderId: vi.fn(), mockGetToolMetadata: vi.fn(), mockRecordAudit: vi.fn(), mockRefreshTokenIfNeeded: vi.fn(), @@ -80,14 +78,7 @@ vi.mock('@/tools/metadata', () => ({ })) vi.mock('@/lib/oauth/utils', () => ({ - credentialProviderMatchesService: ( - credentialProviderId: string, - service: { providerId: string; serviceAccountProviderId?: string } - ) => - credentialProviderId === service.providerId || - credentialProviderId === service.serviceAccountProviderId, getCanonicalScopesForProvider: vi.fn().mockReturnValue([]), - getServiceConfigByProviderId: mockGetServiceConfigByProviderId, })) import { OrchestrationError } from '@/lib/core/orchestration/types' @@ -360,10 +351,6 @@ describe('resolveCredentialAccessToken', () => { mockGetToolMetadata.mockReturnValue({ oauth: { required: true, provider: 'google', requiredScopes: ['scope-a'] }, }) - mockGetServiceConfigByProviderId.mockReturnValue({ - providerId: 'google', - serviceAccountProviderId: 'google-service-account', - }) }) it('authenticates and delegates non-managed credentials without a second account lookup', async () => { @@ -424,212 +411,6 @@ describe('resolveCredentialAccessToken', () => { }) }) - it('rejects a credential whose provider does not match the tool service', async () => { - mockResolveOAuthAccountId.mockResolvedValue({ - accountId: 'account-1', - credentialId: 'credential-1', - credentialType: 'service_account', - providerId: 'oracle-epm-service-account', - usedCredentialTable: true, - }) - mockAuthorizeCredentialUseForAuth.mockResolvedValue({ - ok: true, - requesterUserId: 'user-1', - credentialOwnerUserId: 'owner-1', - workspaceId: 'ws-1', - resolvedCredentialId: 'credential-1', - }) - - const result = await resolveCredentialAccessToken({ - requestId: 'req-1', - credentialId: 'credential-1', - toolId: 'gmail_send', - authenticate, - }) - - expect(result).toEqual({ - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - }) - expect(mockGetServiceConfigByProviderId).toHaveBeenCalledWith('google') - expect(authenticate).toHaveBeenCalledTimes(1) - expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() - }) - - it('does not reveal provider mismatches before credential authorization succeeds', async () => { - mockResolveOAuthAccountId.mockResolvedValue({ - accountId: 'credential-1', - credentialId: 'credential-1', - credentialType: 'service_account', - providerId: 'oracle-epm-service-account', - usedCredentialTable: true, - }) - mockAuthorizeCredentialUseForAuth.mockResolvedValue({ ok: false, error: 'Unauthorized' }) - - await expect( - resolveCredentialAccessToken({ - requestId: 'req-1', - credentialId: 'credential-1', - toolId: 'gmail_send', - authenticate, - }) - ).resolves.toEqual({ ok: false, status: 403, error: 'Unauthorized' }) - expect(authenticate).toHaveBeenCalledTimes(1) - expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() - }) - - it('accepts a shared service-account provider registered by the tool service', async () => { - mockResolveOAuthAccountId.mockResolvedValue({ - accountId: 'credential-1', - credentialId: 'credential-1', - credentialType: 'service_account', - providerId: 'oracle-epm-service-account', - usedCredentialTable: true, - }) - mockGetServiceConfigByProviderId.mockReturnValue({ - providerId: 'synthetic-oracle-child', - serviceAccountProviderId: 'oracle-epm-service-account', - }) - mockAuthorizeCredentialUseForAuth.mockResolvedValue({ - ok: true, - requesterUserId: 'user-1', - credentialOwnerUserId: 'owner-1', - workspaceId: 'ws-1', - resolvedCredentialId: 'credential-1', - }) - mockResolveServiceAccountToken.mockResolvedValue({ - accessToken: 'basic-token', - instanceUrl: 'https://epm.example.com', - }) - - const result = await resolveCredentialAccessToken({ - requestId: 'req-1', - credentialId: 'credential-1', - toolId: 'synthetic_oracle_tool', - authenticate, - }) - - expect(result).toEqual({ - ok: true, - token: { - accessToken: 'basic-token', - credentialType: 'service_account', - instanceUrl: 'https://epm.example.com', - }, - }) - }) - - it('preserves an existing service-account tool without OAuth service metadata', async () => { - mockResolveOAuthAccountId.mockResolvedValue({ - accountId: 'credential-1', - credentialId: 'credential-1', - credentialType: 'service_account', - providerId: 'claude-platform-service-account', - usedCredentialTable: true, - }) - mockGetToolMetadata.mockReturnValue({ oauth: undefined }) - mockAuthorizeCredentialUseForAuth.mockResolvedValue({ - ok: true, - requesterUserId: 'user-1', - credentialOwnerUserId: 'owner-1', - workspaceId: 'ws-1', - resolvedCredentialId: 'credential-1', - }) - mockResolveServiceAccountToken.mockResolvedValue({ accessToken: 'workspace-api-key' }) - - await expect( - resolveCredentialAccessToken({ - requestId: 'req-1', - credentialId: 'credential-1', - toolId: 'managed_agent_run_session', - authenticate, - }) - ).resolves.toEqual({ - ok: true, - token: { - accessToken: 'workspace-api-key', - credentialType: 'service_account', - apiDomain: undefined, - authStyle: undefined, - cloudId: undefined, - domain: undefined, - instanceUrl: undefined, - }, - }) - expect(mockGetServiceConfigByProviderId).not.toHaveBeenCalled() - expect(mockResolveServiceAccountToken).toHaveBeenCalledWith( - 'credential-1', - 'claude-platform-service-account', - [], - undefined - ) - }) - - it('fails closed when declared tool service metadata has no registered service', async () => { - mockResolveOAuthAccountId.mockResolvedValue({ - accountId: 'credential-1', - credentialId: 'credential-1', - credentialType: 'service_account', - providerId: 'oracle-epm-service-account', - usedCredentialTable: true, - }) - mockGetServiceConfigByProviderId.mockReturnValue(null) - mockAuthorizeCredentialUseForAuth.mockResolvedValue({ - ok: true, - requesterUserId: 'user-1', - credentialOwnerUserId: 'owner-1', - workspaceId: 'ws-1', - resolvedCredentialId: 'credential-1', - }) - - await expect( - resolveCredentialAccessToken({ - requestId: 'req-1', - credentialId: 'credential-1', - toolId: 'synthetic_oracle_tool', - authenticate, - }) - ).resolves.toEqual({ - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - }) - expect(authenticate).toHaveBeenCalledTimes(1) - expect(mockResolveServiceAccountToken).not.toHaveBeenCalled() - }) - - it('rejects a mismatched OAuth account after loading its authoritative provider', async () => { - mockResolveOAuthAccountId.mockResolvedValue({ - accountId: 'account-1', - usedCredentialTable: true, - }) - mockAuthorizeCredentialUseForAuth.mockResolvedValue({ - ok: true, - requesterUserId: 'user-1', - credentialOwnerUserId: 'owner-1', - resolvedCredentialId: 'account-1', - }) - mockGetCredential.mockResolvedValue({ providerId: 'salesforce' }) - - const result = await resolveCredentialAccessToken({ - requestId: 'req-1', - credentialId: 'credential-1', - toolId: 'gmail_send', - authenticate, - }) - - expect(result).toEqual({ - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - }) - expect(mockRefreshTokenIfNeeded).not.toHaveBeenCalled() - }) - it('rejects a managed credential when no delegation resolver is wired', async () => { mockResolveOAuthAccountId.mockResolvedValue(MANAGED_RESOLVED) diff --git a/apps/sim/lib/oauth/token-resolution.ts b/apps/sim/lib/oauth/token-resolution.ts index 4e7db06de0f..dfd1f682b5b 100644 --- a/apps/sim/lib/oauth/token-resolution.ts +++ b/apps/sim/lib/oauth/token-resolution.ts @@ -24,12 +24,7 @@ import { MICROSOFT_DATAVERSE_PROVIDER_ID, } from '@/lib/oauth/microsoft-dataverse' import { extractSalesforceInstanceUrl, isSalesforceOAuthProviderId } from '@/lib/oauth/salesforce' -import { - credentialProviderMatchesService, - getCanonicalScopesForProvider, - getServiceConfigByProviderId, - type ServiceProviderIdentity, -} from '@/lib/oauth/utils' +import { getCanonicalScopesForProvider } from '@/lib/oauth/utils' import { captureServerEvent } from '@/lib/posthog/server' import { getToolMetadata } from '@/tools/metadata' import { extractZohoDeskBaseFromScope } from '@/tools/zoho_desk/host-allowlist' @@ -64,12 +59,6 @@ export interface ResolveCredentialTokenInput { auditRequest?: CredentialAuditRequest /** Credential lookup already performed by {@link resolveCredentialAccessToken}'s dispatch. */ resolvedCredential: ResolvedCredential | null - /** - * Trusted tool-service identity used to reject cross-service credentials. - * `null` means the tool declared a service that is absent from the registry; - * `undefined` means the tool declares no OAuth service constraint. - */ - expectedService?: ServiceProviderIdentity | null } export type ResolveCredentialTokenResult = @@ -217,33 +206,10 @@ export async function resolveCredentialToken( workflowId, callerUserId, }) - const isServiceAccount = - resolved?.credentialType === 'service_account' && Boolean(resolved.credentialId) - - if (!authz.ok || (!isServiceAccount && !authz.credentialOwnerUserId)) { - return { ok: false, status: 403, error: authz.error || 'Unauthorized' } - } - if (input.expectedService === null) { - return { - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - } - } - if (isServiceAccount && resolved?.credentialId) { - if ( - input.expectedService && - (!resolved.providerId || - !credentialProviderMatchesService(resolved.providerId, input.expectedService)) - ) { - return { - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - } + if (resolved?.credentialType === 'service_account' && resolved.credentialId) { + if (!authz.ok) { + return { ok: false, status: 403, error: authz.error || 'Unauthorized' } } const saActorId = authz.requesterUserId @@ -318,9 +284,10 @@ export async function resolveCredentialToken( } } - if (!authz.credentialOwnerUserId) { - return { ok: false, status: 403, error: 'Unauthorized' } + if (!authz.ok || !authz.credentialOwnerUserId) { + return { ok: false, status: 403, error: authz.error || 'Unauthorized' } } + const resolvedCredentialId = authz.resolvedCredentialId || credentialId const credential = await getCredential( requestId, @@ -331,17 +298,6 @@ export async function resolveCredentialToken( if (!credential) { return { ok: false, status: 404, error: 'Credential not found' } } - if ( - input.expectedService && - !credentialProviderMatchesService(credential.providerId, input.expectedService) - ) { - return { - ok: false, - status: 403, - code: 'CREDENTIAL_PROVIDER_MISMATCH', - error: 'Credential does not match the tool service', - } - } return completeOAuthCredentialToken({ requestId, @@ -388,10 +344,6 @@ export async function resolveCredentialAccessToken( const { requestId, credentialId, toolId, auditRequest } = input const resolved = credentialId ? await resolveOAuthAccountId(credentialId) : null - const toolMetadata = toolId ? getToolMetadata(toolId) : undefined - const expectedService = toolMetadata?.oauth?.required - ? (getServiceConfigByProviderId(toolMetadata.oauth.provider) ?? null) - : undefined if (resolved?.credentialType !== 'managed_oauth' || !resolved.credentialId) { const auth = await input.authenticate() @@ -409,7 +361,6 @@ export async function resolveCredentialAccessToken( callerUserId: input.callerUserId, auditRequest, resolvedCredential: resolved, - expectedService, }) } @@ -444,8 +395,8 @@ export async function resolveCredentialAccessToken( } } - const managedToolMetadata = toolMetadata - if (!managedToolMetadata?.oauth?.required) { + const toolMetadata = getToolMetadata(toolId) + if (!toolMetadata?.oauth?.required) { logger.error(`[${requestId}] Tool is not configured for managed OAuth`, { toolId }) return { ok: false, @@ -455,12 +406,11 @@ export async function resolveCredentialAccessToken( } } const requiredScopes = - managedToolMetadata.oauth.requiredScopes ?? - getCanonicalScopesForProvider(managedToolMetadata.oauth.provider) + toolMetadata.oauth.requiredScopes ?? getCanonicalScopesForProvider(toolMetadata.oauth.provider) if (requiredScopes.length === 0) { logger.error(`[${requestId}] Tool has no trusted OAuth scope policy`, { toolId, - providerId: managedToolMetadata.oauth.provider, + providerId: toolMetadata.oauth.provider, }) return { ok: false, @@ -475,7 +425,7 @@ export async function resolveCredentialAccessToken( principal, input: { credentialId: resolved.credentialId, - expectedProviderId: managedToolMetadata.oauth.provider, + expectedProviderId: toolMetadata.oauth.provider, requiredScopes, toolId, }, @@ -489,7 +439,7 @@ export async function resolveCredentialAccessToken( 'credential_used', { credential_type: 'managed_oauth', - provider_id: managedToolMetadata.oauth.provider, + provider_id: toolMetadata.oauth.provider, workspace_id: principal.workspaceId, }, { groups: { workspace: principal.workspaceId } } From 3f631c680c97fc0994bb8321812326a308c0ffd3 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 10:12:37 -0700 Subject: [PATCH 08/20] refactor(oracle-epm): defer shared tool auth helper --- apps/sim/lib/oauth/credential-service.test.ts | 81 +------------------ apps/sim/tools/shared/oracle-epm.test.ts | 38 --------- apps/sim/tools/shared/oracle-epm.ts | 60 -------------- 3 files changed, 1 insertion(+), 178 deletions(-) delete mode 100644 apps/sim/tools/shared/oracle-epm.test.ts delete mode 100644 apps/sim/tools/shared/oracle-epm.ts diff --git a/apps/sim/lib/oauth/credential-service.test.ts b/apps/sim/lib/oauth/credential-service.test.ts index eab5027e42f..337b56aa435 100644 --- a/apps/sim/lib/oauth/credential-service.test.ts +++ b/apps/sim/lib/oauth/credential-service.test.ts @@ -7,8 +7,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ coalesceLocally: vi.fn(), - clientCredentialMinter: vi.fn(), - decryptSecret: vi.fn(), getFreshestSlackChain: vi.fn(), getRecentTerminalError: vi.fn(), logger: { @@ -35,19 +33,6 @@ vi.mock('@/lib/concurrency/leader-lock', () => ({ withLeaderLock: mocks.withLeaderLock, })) -vi.mock('@/lib/core/security/encryption', () => ({ - decryptSecret: mocks.decryptSecret, -})) - -vi.mock('@/lib/credentials/client-credential-accounts/server', async (importOriginal) => { - const actual = - await importOriginal() - return { - ...actual, - getClientCredentialAccountMinter: vi.fn(() => mocks.clientCredentialMinter), - } -}) - vi.mock('@/lib/oauth/instagram', () => ({ isInstagramProvider: vi.fn(() => false), shouldProactivelyRefreshInstagramToken: vi.fn(() => false), @@ -79,10 +64,7 @@ vi.mock('@/lib/oauth/terminal-errors', () => ({ markCredentialDead: vi.fn(), })) -import { - resolveCredentialTokenBundle, - resolveServiceAccountToken, -} from '@/lib/oauth/credential-service' +import { resolveCredentialTokenBundle } from '@/lib/oauth/credential-service' const RAW_CREDENTIAL_ID = 'credential-raw-secret-id' const RAW_ACCOUNT_ID = 'account-raw-secret-id' @@ -218,64 +200,3 @@ describe('resolveCredentialTokenBundle selector privacy', () => { expect(slack.logs).toContain(RAW_PROVIDER_ERROR) }) }) - -describe('Oracle EPM client-credential token cache', () => { - const providerId = 'oracle-epm-service-account' - const blob = JSON.stringify({ - type: 'client_credential_account', - providerId, - clientId: 'integration.user@example.com', - clientSecret: 'password', - orgId: 'https://epm.example.com', - }) - - beforeEach(() => { - vi.clearAllMocks() - resetDbChainMock() - mocks.coalesceLocally.mockImplementation( - async (_key: string, producer: () => Promise) => producer() - ) - mocks.decryptSecret.mockResolvedValue({ decrypted: blob }) - }) - - it('reuses the conservative synthetic token while its safety window remains', async () => { - const credentialId = 'oracle-epm-cache-credential' - const encrypted = 'cache-secret-fingerprint-000000000000000000000000000000000' - queueTableRows(credential, [{ encryptedServiceAccountKey: encrypted }]) - queueTableRows(credential, [{ encryptedServiceAccountKey: encrypted }]) - mocks.clientCredentialMinter.mockResolvedValue({ - accessToken: 'basic-token', - expiresInSeconds: 600, - instanceUrl: 'https://epm.example.com', - }) - - await expect(resolveServiceAccountToken(credentialId, providerId)).resolves.toMatchObject({ - accessToken: 'basic-token', - }) - await expect(resolveServiceAccountToken(credentialId, providerId)).resolves.toMatchObject({ - accessToken: 'basic-token', - }) - expect(mocks.clientCredentialMinter).toHaveBeenCalledTimes(1) - }) - - it('invalidates the cached token immediately when encrypted credentials rotate', async () => { - const credentialId = 'oracle-epm-rotation-credential' - queueTableRows(credential, [ - { encryptedServiceAccountKey: 'old-secret-fingerprint-000000000000000000000000000000000' }, - ]) - queueTableRows(credential, [ - { encryptedServiceAccountKey: 'new-secret-fingerprint-000000000000000000000000000000000' }, - ]) - mocks.clientCredentialMinter - .mockResolvedValueOnce({ accessToken: 'old-token', expiresInSeconds: 600 }) - .mockResolvedValueOnce({ accessToken: 'new-token', expiresInSeconds: 600 }) - - await expect(resolveServiceAccountToken(credentialId, providerId)).resolves.toMatchObject({ - accessToken: 'old-token', - }) - await expect(resolveServiceAccountToken(credentialId, providerId)).resolves.toMatchObject({ - accessToken: 'new-token', - }) - expect(mocks.clientCredentialMinter).toHaveBeenCalledTimes(2) - }) -}) diff --git a/apps/sim/tools/shared/oracle-epm.test.ts b/apps/sim/tools/shared/oracle-epm.test.ts deleted file mode 100644 index f2018ef72ea..00000000000 --- a/apps/sim/tools/shared/oracle-epm.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** @vitest-environment node */ -import { describe, expect, it, vi } from 'vitest' - -const { getServiceConfig } = vi.hoisted(() => ({ getServiceConfig: vi.fn() })) -vi.mock('@/lib/oauth/utils', () => ({ getServiceConfigByServiceId: getServiceConfig })) - -import { createOracleEpmAuthParameters } from '@/tools/shared/oracle-epm' - -describe('createOracleEpmAuthParameters', () => { - it('returns a fresh deeply frozen bundle tied to the child service id', () => { - getServiceConfig.mockReturnValue({ - providerId: 'synthetic-provider', - serviceAccountProviderId: 'oracle-epm-service-account', - }) - const first = createOracleEpmAuthParameters({ serviceId: 'jira' }) - const second = createOracleEpmAuthParameters({ serviceId: 'jira' }) - - expect(first).not.toBe(second) - expect(first.params).not.toBe(second.params) - expect(first.oauth.provider).toBe('jira') - expect(first.oauth).toMatchObject({ - credentialKind: 'service-account', - authoritativeParams: ['instanceUrl'], - }) - expect(first.params.oauthCredential.visibility).toBe('user-only') - expect(first.params.accessToken.visibility).toBe('hidden') - expect(Object.isFrozen(first)).toBe(true) - expect(Object.isFrozen(first.params.oauthCredential)).toBe(true) - expect(Object.isFrozen(first.oauth.authoritativeParams)).toBe(true) - }) - - it('fails at declaration time when the service is absent or mapped elsewhere', () => { - getServiceConfig.mockReturnValue(null) - expect(() => createOracleEpmAuthParameters({ serviceId: 'jira' })).toThrow('not registered') - getServiceConfig.mockReturnValue({ serviceAccountProviderId: 'another-provider' }) - expect(() => createOracleEpmAuthParameters({ serviceId: 'jira' })).toThrow('not registered') - }) -}) diff --git a/apps/sim/tools/shared/oracle-epm.ts b/apps/sim/tools/shared/oracle-epm.ts deleted file mode 100644 index 868c690f9e1..00000000000 --- a/apps/sim/tools/shared/oracle-epm.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/credentials/client-credential-accounts/descriptors' -import type { OAuthService } from '@/lib/oauth' -import { getServiceConfigByServiceId } from '@/lib/oauth/utils' -import type { OAuthConfig, ParameterVisibility } from '@/tools/types' - -interface OracleEpmAuthParameter { - readonly type: 'string' - readonly required: boolean - readonly visibility: ParameterVisibility - readonly description: string -} - -export interface OracleEpmAuthParameters { - readonly params: Readonly<{ - oauthCredential: OracleEpmAuthParameter - accessToken: OracleEpmAuthParameter - instanceUrl: OracleEpmAuthParameter - }> - readonly oauth: Readonly -} - -/** - * Creates one child integration's service-account contract. Calling this from - * a child module validates its registered service mapping immediately. - */ -export function createOracleEpmAuthParameters(input: { - serviceId: OAuthService -}): OracleEpmAuthParameters { - const service = getServiceConfigByServiceId(input.serviceId) - if (service?.serviceAccountProviderId !== ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID) { - throw new Error('Oracle EPM service is not registered with the Oracle EPM credential provider') - } - - const oauthCredential = Object.freeze({ - type: 'string', - required: true, - visibility: 'user-only', - description: 'Oracle EPM integration-user credential', - } as const) - const accessToken = Object.freeze({ - type: 'string', - required: true, - visibility: 'hidden', - description: 'Credential-resolved Oracle EPM Basic authorization value', - } as const) - const instanceUrl = Object.freeze({ - type: 'string', - required: true, - visibility: 'hidden', - description: 'Credential-bound Oracle EPM environment URL', - } as const) - const params = Object.freeze({ oauthCredential, accessToken, instanceUrl }) - const oauth = Object.freeze({ - required: true, - provider: input.serviceId, - credentialKind: 'service-account', - authoritativeParams: Object.freeze(['instanceUrl'] as const), - }) - return Object.freeze({ params, oauth }) -} From fa2fc7fb73d7443bec8a2752b0531b1c95316fb6 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 10:15:37 -0700 Subject: [PATCH 09/20] refactor(oracle-epm): keep API contract tests provider-neutral --- .../sim/lib/api/contracts/credentials.test.ts | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/apps/sim/lib/api/contracts/credentials.test.ts b/apps/sim/lib/api/contracts/credentials.test.ts index 38749d0da24..ae6f6406ee7 100644 --- a/apps/sim/lib/api/contracts/credentials.test.ts +++ b/apps/sim/lib/api/contracts/credentials.test.ts @@ -3,7 +3,6 @@ */ import { describe, expect, it } from 'vitest' import { - createCredentialBodySchema, updateCredentialByIdBodySchema, workspaceCredentialSchema, } from '@/lib/api/contracts/credentials' @@ -47,24 +46,3 @@ describe('workspaceCredentialSchema unredacted', () => { expect(workspaceCredentialSchema.safeParse(credential).success).toBe(false) }) }) - -describe('Oracle EPM service-account credential contract', () => { - const valid = { - workspaceId: '00000000-0000-4000-8000-000000000001', - type: 'service_account' as const, - providerId: 'oracle-epm-service-account', - orgId: 'https://epm.example.com/gateway', - clientId: 'integration.user@example.com', - clientSecret: 'password', - } - - it('accepts the descriptor-required integration-user fields', () => { - expect(createCredentialBodySchema.safeParse(valid).success).toBe(true) - }) - - it.each(['orgId', 'clientId', 'clientSecret'] as const)('rejects a missing %s', (field) => { - expect(createCredentialBodySchema.safeParse({ ...valid, [field]: undefined }).success).toBe( - false - ) - }) -}) From ad88fb25e36c52aa65d300b226c496ddad4e126d Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 13:43:22 -0700 Subject: [PATCH 10/20] fix(oracle-epm): clarify credential REST base URL --- .../credentials/client-credential-accounts/descriptors.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts b/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts index 7fd4f83abb2..7e7cb68f5bb 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts @@ -540,11 +540,12 @@ export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record< fields: [ { id: 'orgId', - label: 'Environment URL', - placeholder: 'https://example.oraclecloud.com/epmcloud', + label: 'REST Base URL', + placeholder: 'https://example.oraclecloud.com', secret: false, hintPattern: /^https:\/\//, - hintMessage: 'Expected the full HTTPS URL for one Oracle EPM environment.', + hintMessage: 'Expected the HTTPS REST base URL for one Oracle EPM environment.', + hint: 'Enter the HTTPS base URL for your environment without /epmcloud or an API endpoint path. Include a gateway prefix only if your deployment requires it.', }, { id: 'clientId', From cb8b98a008fa56ae4b41d77996d900a0b2faa3f6 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 14:01:43 -0700 Subject: [PATCH 11/20] fix(oracle-epm): correct credential authentication docs link --- .../lib/credentials/client-credential-accounts/descriptors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts b/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts index 7e7cb68f5bb..a070a325e54 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/descriptors.ts @@ -561,7 +561,7 @@ export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record< }, ], docsUrl: - 'https://docs.oracle.com/en/cloud/saas/enterprise-performance-management-common/prest/overview.html', + 'https://docs.oracle.com/en/cloud/saas/enterprise-performance-management-common/prest/authentication.html', helpText: 'The credential is bound to one EPM environment. Use a dedicated integration user with only the permissions its workflows require.', }, From b81803b2a3f3a055521c36da13ee42ae04b5e8a4 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 14:47:43 -0700 Subject: [PATCH 12/20] feat(oracle-epm): support repository-path parameters --- .../internal/oracle-epm/client.server.test.ts | 301 +++++++++++++++++- .../lib/internal/oracle-epm/client.server.ts | 70 ++-- .../lib/internal/oracle-epm/endpoint.test.ts | 47 +++ apps/sim/lib/internal/oracle-epm/endpoint.ts | 7 +- apps/sim/lib/internal/oracle-epm/types.ts | 3 + 5 files changed, 399 insertions(+), 29 deletions(-) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index 2f2bbcc02a7..14315c9e7f5 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -21,7 +21,10 @@ import { } from '@/lib/internal/oracle-epm/endpoint' import { OracleEpmError } from '@/lib/internal/oracle-epm/errors' import { defineOracleEpmRouteSpace } from '@/lib/internal/oracle-epm/route-space' -import type { OracleEpmValidatedLink } from '@/lib/internal/oracle-epm/types' +import type { + OracleEpmEndpointDeclaration, + OracleEpmValidatedLink, +} from '@/lib/internal/oracle-epm/types' const routes = defineOracleEpmRouteSpace({ context: ['SyntheticAlpha', 'rest'], @@ -419,4 +422,300 @@ describe('Oracle EPM guarded client', () => { OracleEpmError ) }) + + describe('repository path parameters', () => { + const declaration = { + method: 'GET', + version: 'v3', + path: [ + oracleEpmLiteral('files'), + oracleEpmPathParameter('fileName', { maxBytes: 255, mode: 'repository-path' }), + oracleEpmLiteral('contents'), + ], + query: { token: oracleEpmQuery.string({ maxBytes: 128 }) }, + body: 'none', + response: 'stream', + timeoutMs: 5_000, + maxResponseBytes: 4_096, + } satisfies OracleEpmEndpointDeclaration + const download = routes.defineEndpoint(declaration) + const prefix = 'https://epm.example.com/gateway/acme/SyntheticAlpha/rest/v3' + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway/acme', + accessToken: Buffer.from('u:p').toString('base64'), + }) + + it.each([ + ['outbox/reports/results.csv', 'outbox%2Freports%2Fresults.csv'], + ['inbox\\Monthly Report.csv', 'inbox%5CMonthly%20Report.csv'], + ['outbox\\reports/results.csv', 'outbox%5Creports%2Fresults.csv'], + ['outbox/résumé-😀.csv', 'outbox%2Fr%C3%A9sum%C3%A9-%F0%9F%98%80.csv'], + [' report.csv ', '%20report.csv%20'], + ['outbox/100%.csv', 'outbox%2F100%25.csv'], + ['outbox%2Freport.csv', 'outbox%252Freport.csv'], + ['outbox/%2e%2e/report.csv', 'outbox%2F%252e%252e%2Freport.csv'], + ])('encodes raw filename %j once without rewriting it', async (fileName, encoded) => { + await client.request(download, { pathParams: { fileName } }) + expect(mockSecureFetch.mock.calls[0][0]).toBe(`${prefix}/files/${encoded}/contents`) + expect(mockValidateUrl).toHaveBeenCalledWith( + 'https://epm.example.com', + 'Oracle EPM destination', + 'configuredEndpoint', + { logDetails: false } + ) + }) + + it.each([ + '', + '/file.csv', + '\\file.csv', + '\\\\server\\file.csv', + 'C:\\file.csv', + 'c:file.csv', + 'outbox//file.csv', + 'outbox\\\\file.csv', + 'outbox/\\file.csv', + 'outbox/', + 'outbox\\', + '.', + '..', + './file.csv', + '../file.csv', + 'outbox/./file.csv', + 'outbox/../file.csv', + 'outbox\\..\\file.csv', + 'outbox/..', + 'outbox/\nfile.csv', + 'outbox/\u0000file.csv', + 'outbox/\u007ffile.csv', + 'outbox/\uD800.csv', + 'a'.repeat(256), + 'é'.repeat(128), + ])('rejects invalid raw filename %j before DNS or fetch', async (fileName) => { + await expect(client.request(download, { pathParams: { fileName } })).rejects.toMatchObject({ + category: 'invalid_input', + }) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + + it('accepts the full 255-byte raw UTF-8 boundary', async () => { + const fileName = `${'é'.repeat(127)}x` + await client.request(download, { pathParams: { fileName } }) + expect(mockSecureFetch.mock.calls[0][0]).toBe( + `${prefix}/files/${encodeURIComponent(fileName)}/contents` + ) + }) + + it.each([undefined, 'segment'] as const)( + 'keeps mode %j strict for ordinary IDs', + async (mode) => { + const endpoint = routes.defineEndpoint({ + ...declaration, + path: [oracleEpmLiteral('jobs'), oracleEpmPathParameter('jobId', { maxBytes: 64, mode })], + }) + for (const jobId of ['folder/id', 'folder\\id']) { + await expect(client.request(endpoint, { pathParams: { jobId } })).rejects.toMatchObject({ + category: 'invalid_input', + }) + } + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + await client.request(endpoint, { pathParams: { jobId: 'job 42' } }) + expect(mockSecureFetch.mock.calls[0][0]).toBe(`${prefix}/jobs/job%2042`) + } + ) + + it('does not let request input select the parameter mode', async () => { + await expect( + client.request(getJob, { pathParams: { jobId: 'folder/id', mode: 'repository-path' } }) + ).rejects.toMatchObject({ category: 'invalid_input' }) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + + describe.each(['endpoint', 'route'] as const)('%s-bound returned links', (binding) => { + function definePolicy(endpointDeclaration = declaration, preserveGatewayBasePath = true) { + return routes.defineReturnedLinkPolicy({ + relation: 'download', + method: 'GET', + ...(binding === 'endpoint' + ? { endpoint: routes.defineEndpoint(endpointDeclaration) } + : { + version: endpointDeclaration.version, + path: endpointDeclaration.path, + query: endpointDeclaration.query, + response: endpointDeclaration.response, + timeoutMs: endpointDeclaration.timeoutMs, + maxResponseBytes: endpointDeclaration.maxResponseBytes, + }), + preserveGatewayBasePath, + }) + } + const policy = definePolicy() + + it.each([ + 'outbox%2Freports%2Fresults.csv', + 'inbox%5CMonthly%20Report.csv', + 'outbox%2fr%C3%A9sum%C3%A9-%F0%9F%98%80.csv', + 'outbox%2F100%25.csv', + 'outbox%2F%2525252561.csv', + `${'%C3%A9'.repeat(127)}x`, + ])('retains filename encoding and query bytes for %s', async (encoded) => { + const href = `${prefix}/files/${encoded}/contents?token=secret%2bvalue` + const link = client.validateReturnedLink(policy, { rel: 'download', href }) + expect(Object.isFrozen(link)).toBe(true) + expect(Object.keys(link)).toEqual([]) + expect(JSON.stringify(link)).toBe('{}') + await client.requestValidatedLink(link) + expect(mockSecureFetch.mock.calls[0][0]).toBe(href) + + const otherClient = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway/acme', + accessToken: Buffer.from('other:p').toString('base64'), + }) + await expect(otherClient.requestValidatedLink(link)).rejects.toMatchObject({ + category: 'invalid_input', + }) + expect(mockSecureFetch).toHaveBeenCalledTimes(1) + }) + + it.each([ + 'outbox/report.csv', + 'outbox\\report.csv', + '%2Freport.csv', + '%5C%5Cserver%5Creport.csv', + 'C%3A%5Creport.csv', + 'c%3Areport.csv', + 'outbox%2F%2Freport.csv', + 'outbox%2F', + 'outbox%2F.%2Freport.csv', + 'outbox%2F..%2Fsecret.csv', + 'outbox%5C..%5Csecret.csv', + 'outbox%2F%252e%252e%2Fsecret.csv', + 'outbox%252F%252e%252e%252Fsecret.csv', + 'outbox%2F%00report.csv', + 'outbox%2F%250areport.csv', + 'outbox%2F%7freport.csv', + 'outbox%2F%ED%A0%80.csv', + 'outbox%2Fbad%.csv', + 'outbox%2F%25FF.csv', + '%C3%A9'.repeat(128), + '%252525252561.csv', + ])('rejects invalid encoded filenames %s', (encoded) => { + expect(() => + client.validateReturnedLink(policy, { + rel: 'download', + href: `${prefix}/files/${encoded}/contents`, + }) + ).toThrow(OracleEpmError) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + + it('validates bounds and patterns against the once-decoded filename', async () => { + const boundedDeclaration = { + ...declaration, + path: [ + oracleEpmPathParameter('fileName', { + maxBytes: 14, + pattern: /^outbox\/%61\.csv$/, + mode: 'repository-path', + }), + ], + } + const endpoint = routes.defineEndpoint(boundedDeclaration) + const boundedPolicy = definePolicy(boundedDeclaration) + const fileName = 'outbox/%61.csv' + const href = `${prefix}/outbox%2F%2561.csv` + await client.request(endpoint, { pathParams: { fileName } }) + const handle = client.validateReturnedLink(boundedPolicy, { rel: 'download', href }) + await client.requestValidatedLink(handle) + expect(mockSecureFetch.mock.calls.map(([url]) => url)).toEqual([href, href]) + for (const invalid of ['outbox/a.csv', 'inbox/%61.csv', 'outbox/long%61.csv']) { + await expect( + client.request(endpoint, { pathParams: { fileName: invalid } }) + ).rejects.toMatchObject({ category: 'invalid_input' }) + expect(() => + client.validateReturnedLink(boundedPolicy, { + rel: 'download', + href: `${prefix}/${encodeURIComponent(invalid)}`, + }) + ).toThrow(OracleEpmError) + } + expect(mockSecureFetch).toHaveBeenCalledTimes(2) + }) + + it('preserves the declared gateway-prefix policy', async () => { + const originHref = + 'https://epm.example.com/SyntheticAlpha/rest/v3/files/outbox%2Freport.csv/contents' + expect(() => + client.validateReturnedLink(policy, { rel: 'download', href: originHref }) + ).toThrow(OracleEpmError) + const originPolicy = definePolicy(declaration, false) + const handle = client.validateReturnedLink(originPolicy, { + rel: 'download', + href: originHref, + }) + await client.requestValidatedLink(handle) + expect(mockSecureFetch.mock.calls[0][0]).toBe(originHref) + expect(() => + client.validateReturnedLink(originPolicy, { + rel: 'download', + href: `${prefix}/files/outbox%2Freport.csv/contents`, + }) + ).toThrow(OracleEpmError) + }) + + it.each([ + [ + 'origin', + `${prefix.replace('epm.example.com', 'other.example.com')}/files/outbox%2Freport.csv/contents`, + ], + [ + 'userinfo', + `${prefix.replace('https://', 'https://user@')}/files/outbox%2Freport.csv/contents`, + ], + [ + 'gateway', + `${prefix.replace('/gateway/acme/', '/gateway%2Facme/')}/files/outbox%2Freport.csv/contents`, + ], + [ + 'context', + `${prefix.replace('/SyntheticAlpha/rest/', '/SyntheticAlpha%2Frest/')}/files/outbox%2Freport.csv/contents`, + ], + ['literal', `${prefix}/files%2Fextra/outbox%2Freport.csv/contents`], + ['suffix', `${prefix}/files/outbox%2Freport.csv/contents%2Fextra`], + ['extra segment', `${prefix}/files/outbox%2Freport.csv/extra/contents`], + ['duplicate query', `${prefix}/files/outbox%2Freport.csv/contents?token=a&token=b`], + ['unknown query', `${prefix}/files/outbox%2Freport.csv/contents?unknown=x`], + ['fragment', `${prefix}/files/outbox%2Freport.csv/contents#fragment`], + ])('preserves the %s restriction', (_label, href) => { + expect(() => client.validateReturnedLink(policy, { rel: 'download', href })).toThrow( + OracleEpmError + ) + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + + it('keeps ordinary parameters, methods, and relations strict on repository endpoints', () => { + const mixedPolicy = definePolicy({ + ...declaration, + path: [...declaration.path, oracleEpmPathParameter('jobId', { maxBytes: 64 })], + }) + const href = `${prefix}/files/outbox%2Freport.csv/contents` + for (const jobId of ['a%2Fb', 'a%5Cb']) { + expect(() => + client.validateReturnedLink(mixedPolicy, { rel: 'download', href: `${href}/${jobId}` }) + ).toThrow(OracleEpmError) + } + expect(() => + client.validateReturnedLink(policy, { rel: 'download', method: 'POST', href }) + ).toThrow(OracleEpmError) + expect(() => client.validateReturnedLink(policy, { rel: 'other', href })).toThrow( + OracleEpmError + ) + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + }) + }) }) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts index a73dd123093..d06160a0b05 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -55,22 +55,38 @@ function assertExactKeys( } } +function validatePathStructure( + value: string, + mode: 'segment' | 'repository-path' = 'segment' +): void { + if (!value || FORBIDDEN_LINK_TEXT.test(value) || MALFORMED_UTF16.test(value)) { + throw oracleEpmLocalError('invalid_input') + } + if (mode === 'repository-path') { + if ( + /^[A-Za-z]:/.test(value) || + value.split(/[/\\]/).some((part) => !part || part === '.' || part === '..') + ) { + throw oracleEpmLocalError('invalid_input') + } + } else if (value === '.' || value === '..' || /[/\\]/.test(value)) { + throw oracleEpmLocalError('invalid_input') + } +} + +/** Validates raw caller input without decoding or rewriting the filename. */ function validatePathValue( value: unknown, declaration: Extract ): string { if ( typeof value !== 'string' || - !value || - value === '.' || - value === '..' || - /[/\\\u0000-\u001f\u007f]/.test(value) || - MALFORMED_UTF16.test(value) || Buffer.byteLength(value, 'utf8') > declaration.maxBytes || (declaration.pattern && !declaration.pattern.test(value)) ) { throw oracleEpmLocalError('invalid_input') } + validatePathStructure(value, declaration.mode) return value } @@ -312,7 +328,11 @@ async function projectResponse( } } -function decodeReturnedPathSegment(encoded: string): string { +/** Decodes one wire segment; additional decoded copies are only inspected for safety. */ +function decodeReturnedPathSegment( + encoded: string, + mode: 'segment' | 'repository-path' = 'segment' +): string { let decoded: string try { decoded = decodeURIComponent(encoded) @@ -320,23 +340,19 @@ function decodeReturnedPathSegment(encoded: string): string { throw oracleEpmLocalError('invalid_input') } let safetyValue = decoded + if (mode === 'repository-path') validatePathStructure(safetyValue, mode) for (let depth = 0; depth < 4 && /%[0-9A-Fa-f]{2}/.test(safetyValue); depth += 1) { try { safetyValue = decodeURIComponent(safetyValue) } catch { throw oracleEpmLocalError('invalid_input') } + if (mode === 'repository-path') validatePathStructure(safetyValue, mode) } - if ( - !decoded || - /%[0-9A-Fa-f]{2}/.test(safetyValue) || - safetyValue === '.' || - safetyValue === '..' || - /[/\\\u0000-\u001f\u007f]/.test(safetyValue) || - MALFORMED_UTF16.test(decoded) - ) { + if (/%[0-9A-Fa-f]{2}/.test(safetyValue)) { throw oracleEpmLocalError('invalid_input') } + validatePathStructure(safetyValue, mode) return decoded } @@ -348,7 +364,7 @@ function rawReturnedPathSegments(href: string): string[] { if (!rawPath) return [] const segments = rawPath.slice(1).split('/') if (segments.some((segment) => !segment)) throw oracleEpmLocalError('invalid_input') - return segments.map(decodeReturnedPathSegment) + return segments } function matchReturnedPath( @@ -357,8 +373,11 @@ function matchReturnedPath( ): void { if (candidate.length !== expected.length) throw oracleEpmLocalError('invalid_input') for (let index = 0; index < expected.length; index += 1) { - const decoded = candidate[index] const part = expected[index] + const decoded = decodeReturnedPathSegment( + candidate[index], + part.kind === 'parameter' ? part.mode : undefined + ) if (part.kind === 'literal') { if (decoded !== part.value) throw oracleEpmLocalError('invalid_input') } else { @@ -519,17 +538,18 @@ export function createOracleEpmClient(input: { if (url.origin !== destinationData.origin || url.username || url.password || url.hash) throw oracleEpmLocalError('invalid_input') const route = getOracleEpmRouteSpace(policy.routeSpace) - const prefix = [...destinationData.baseSegments, ...route.context, policy.version] - const prefixMatches = (expected: readonly string[]): boolean => - expected.every((part, index) => candidate[index] === part) - if (policy.preserveGatewayBasePath && !prefixMatches(prefix)) + const prefix = [ + ...(policy.preserveGatewayBasePath ? destinationData.baseSegments : []), + ...route.context, + policy.version, + ] + if ( + candidate.length !== prefix.length + policy.path.length || + prefix.some((part, index) => decodeReturnedPathSegment(candidate[index]) !== part) + ) { throw oracleEpmLocalError('invalid_input') - const pathStart = policy.preserveGatewayBasePath ? prefix.length : route.context.length + 1 - if (!policy.preserveGatewayBasePath) { - const routePrefix = [...route.context, policy.version] - if (!prefixMatches(routePrefix)) throw oracleEpmLocalError('invalid_input') } - matchReturnedPath(candidate.slice(pathStart), policy.path) + matchReturnedPath(candidate.slice(prefix.length), policy.path) const seen = new Set() for (const [name, value] of url.searchParams) { if (seen.has(name) || !Object.hasOwn(policy.query, name)) diff --git a/apps/sim/lib/internal/oracle-epm/endpoint.test.ts b/apps/sim/lib/internal/oracle-epm/endpoint.test.ts index efa4c700395..12e6229674c 100644 --- a/apps/sim/lib/internal/oracle-epm/endpoint.test.ts +++ b/apps/sim/lib/internal/oracle-epm/endpoint.test.ts @@ -34,6 +34,53 @@ describe('Oracle EPM endpoints', () => { expect(Object.isFrozen(declaration.query)).toBe(true) }) + it.each([undefined, 'segment', 'repository-path'] as const)( + 'preserves and freezes path parameter mode %j', + (mode) => { + const endpoint = routes.defineEndpoint({ + method: 'GET', + version: 'V1', + path: [oracleEpmPathParameter('fileName', { maxBytes: 255, mode })], + body: 'none', + response: 'stream', + timeoutMs: 1_000, + maxResponseBytes: 1_024, + }) + const parameter = getOracleEpmEndpoint(endpoint).path[0] + expect(parameter).toEqual({ kind: 'parameter', name: 'fileName', maxBytes: 255, mode }) + expect(Object.isFrozen(parameter)).toBe(true) + expect(Reflect.set(parameter, 'mode', 'unexpected')).toBe(false) + } + ) + + it.each(['', 'repository', 'SEGMENT', null, 1])('rejects invalid path mode %j', (mode) => { + expect(() => + routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmPathParameter('fileName', { maxBytes: 255, mode: mode as 'segment' })], + body: 'none', + response: 'stream', + timeoutMs: 1_000, + maxResponseBytes: 1_024, + }) + ).toThrow('path parameter declaration') + }) + + it('keeps the 255-byte declaration ceiling for repository paths', () => { + expect(() => + routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmPathParameter('fileName', { maxBytes: 256, mode: 'repository-path' })], + body: 'none', + response: 'stream', + timeoutMs: 1_000, + maxResponseBytes: 1_024, + }) + ).toThrow('path parameter declaration') + }) + it('rejects versions with the wrong case and dangerous headers', () => { expect(() => routes.defineEndpoint({ diff --git a/apps/sim/lib/internal/oracle-epm/endpoint.ts b/apps/sim/lib/internal/oracle-epm/endpoint.ts index a236bf48a78..54546e3f25e 100644 --- a/apps/sim/lib/internal/oracle-epm/endpoint.ts +++ b/apps/sim/lib/internal/oracle-epm/endpoint.ts @@ -67,7 +67,8 @@ function validatePath(path: readonly OracleEpmPathPart[]): void { names.has(part.name) || !Number.isInteger(part.maxBytes) || part.maxBytes < 1 || - part.maxBytes > MAX_LITERAL_BYTES + part.maxBytes > MAX_LITERAL_BYTES || + (part.mode !== undefined && part.mode !== 'segment' && part.mode !== 'repository-path') ) { throw new Error('Oracle EPM endpoint path parameter declaration is invalid') } @@ -186,10 +187,10 @@ export function oracleEpmLiteral(value: string): OracleEpmPathPart { return Object.freeze({ kind: 'literal', value }) } -/** Defines one individually encoded path parameter. */ +/** Defines one raw path parameter, encoded once even when it contains repository folders. */ export function oracleEpmPathParameter( name: string, - options: { maxBytes: number; pattern?: RegExp } + options: { maxBytes: number; pattern?: RegExp; mode?: 'segment' | 'repository-path' } ): OracleEpmPathPart { return Object.freeze({ kind: 'parameter', name, ...options }) } diff --git a/apps/sim/lib/internal/oracle-epm/types.ts b/apps/sim/lib/internal/oracle-epm/types.ts index e2d6370361a..6d286634c88 100644 --- a/apps/sim/lib/internal/oracle-epm/types.ts +++ b/apps/sim/lib/internal/oracle-epm/types.ts @@ -17,8 +17,11 @@ export type OracleEpmPathPart = | { readonly kind: 'parameter' readonly name: string + /** Maximum UTF-8 bytes in the complete raw parameter, capped at 255. */ readonly maxBytes: number readonly pattern?: RegExp + /** Defaults to segment; repository paths remain one encoded parameter. */ + readonly mode?: 'segment' | 'repository-path' } /** Bounded scalar query input admitted by an endpoint or returned-link policy. */ From a818f7af77c90e3575dc04f8eae1a422823e5d6f Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 15:25:23 -0700 Subject: [PATCH 13/20] fix(oracle-epm): support multiword returned-link relations --- .../internal/oracle-epm/client.server.test.ts | 173 +++++++++++------- .../sim/lib/internal/oracle-epm/links.test.ts | 94 ++++++++-- apps/sim/lib/internal/oracle-epm/links.ts | 9 +- 3 files changed, 199 insertions(+), 77 deletions(-) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index 14315c9e7f5..5c4bb78f088 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -308,75 +308,106 @@ describe('Oracle EPM guarded client', () => { expect(mockSecureFetch).not.toHaveBeenCalled() }) - it('returns an opaque same-client link capability and keeps query secrets out of serialization', async () => { - const download = routes.defineEndpoint({ + it.each(['download', 'Job Status'])( + 'keeps %s links opaque and client-owned', + async (relation) => { + const download = routes.defineEndpoint({ + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], + query: { token: oracleEpmQuery.string({ required: true, maxBytes: 128 }) }, + body: 'none', + response: 'stream', + timeoutMs: 5_000, + maxResponseBytes: 4_096, + }) + const policy = routes.defineReturnedLinkPolicy({ + relation, + method: 'GET', + endpoint: download, + preserveGatewayBasePath: true, + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('user:password').toString('base64'), + }) + const secret = 'signed-query-secret' + const link = client.validateReturnedLink(policy, { + rel: relation, + href: `https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=${secret}`, + }) + + expect(Object.isFrozen(link)).toBe(true) + expect(Object.keys(link)).toEqual([]) + expect(JSON.stringify(link)).toBe('{}') + expect(String(link)).not.toContain(secret) + + mockSecureFetch.mockResolvedValue(secureResponse({ body: new ReadableStream() })) + await client.requestValidatedLink(link) + expect(mockSecureFetch).toHaveBeenCalledWith( + `https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=${secret}`, + '203.0.113.10', + expect.objectContaining({ method: 'GET' }) + ) + + const otherClient = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('other:password').toString('base64'), + }) + await expect(otherClient.requestValidatedLink(link)).rejects.toBeInstanceOf(OracleEpmError) + expect(mockValidateUrl).toHaveBeenCalledTimes(1) + expect(mockSecureFetch).toHaveBeenCalledTimes(1) + } + ) + + it.each( + [ + 'https://evil.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://user@epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://epm.example.com/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x&token=y', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?unknown=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#fragment', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/ab\nc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/\uD800?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest//v3/files/abc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc/?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/./abc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e%2e?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e.?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%252e%252e?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%2Fabc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%5Cabc?token=x', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files\\abc?token=x', + ].flatMap((href) => ['download', 'Job Status'].map((relation) => ({ relation, href }))) + )('rejects unsafe $relation link $href', ({ relation, href }) => { + const policy = routes.defineReturnedLinkPolicy({ + relation, method: 'GET', version: 'v3', path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], query: { token: oracleEpmQuery.string({ required: true, maxBytes: 128 }) }, - body: 'none', response: 'stream', timeoutMs: 5_000, maxResponseBytes: 4_096, - }) - const policy = routes.defineReturnedLinkPolicy({ - relation: 'download', - method: 'GET', - endpoint: download, preserveGatewayBasePath: true, }) const client = createOracleEpmClient({ instanceUrl: 'https://epm.example.com/gateway', - accessToken: Buffer.from('user:password').toString('base64'), - }) - const secret = 'signed-query-secret' - const link = client.validateReturnedLink(policy, { - rel: 'download', - href: `https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=${secret}`, - }) - - expect(Object.isFrozen(link)).toBe(true) - expect(Object.keys(link)).toEqual([]) - expect(JSON.stringify(link)).toBe('{}') - expect(String(link)).not.toContain(secret) - - mockSecureFetch.mockResolvedValue(secureResponse({ body: new ReadableStream() })) - await client.requestValidatedLink(link) - expect(mockSecureFetch).toHaveBeenCalledTimes(1) - - const otherClient = createOracleEpmClient({ - instanceUrl: 'https://epm.example.com/gateway', - accessToken: Buffer.from('other:password').toString('base64'), + accessToken: Buffer.from('u:p').toString('base64'), }) - await expect(otherClient.requestValidatedLink(link)).rejects.toBeInstanceOf(OracleEpmError) + expect(() => client.validateReturnedLink(policy, { rel: relation, href })).toThrow() + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() }) - it.each([ - 'https://evil.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', - 'https://user@epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', - 'https://epm.example.com/SyntheticAlpha/rest/v3/files/abc?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x&token=y', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?unknown=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#fragment', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/ab\nc?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/\uD800?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest//v3/files/abc?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc/?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/./abc?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e%2e?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e.?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%252e%252e?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%2Fabc?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%5Cabc?token=x', - 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files\\abc?token=x', - ])('rejects unsafe returned link %j', (href) => { + it.each(['download', 'Job Status'])('rejects an incorrect %s link method', (relation) => { const policy = routes.defineReturnedLinkPolicy({ - relation: 'download', + relation, method: 'GET', version: 'v3', path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], - query: { token: oracleEpmQuery.string({ required: true, maxBytes: 128 }) }, response: 'stream', timeoutMs: 5_000, maxResponseBytes: 4_096, @@ -386,31 +417,47 @@ describe('Oracle EPM guarded client', () => { instanceUrl: 'https://epm.example.com/gateway', accessToken: Buffer.from('u:p').toString('base64'), }) - expect(() => client.validateReturnedLink(policy, { rel: 'download', href })).toThrow() + expect(() => + client.validateReturnedLink(policy, { + rel: relation, + method: 'POST', + href: 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc', + }) + ).toThrow() + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() }) - it('rejects an incorrect returned-link method', () => { + it.each([ + 'job status', + 'Job status', + ' Job Status', + 'Job Status ', + 'Job Status', + 'Job\tStatus', + 'Job Status\n', + 'download', + ])('rejects nonmatching relation %j before DNS or network access', (rel) => { const policy = routes.defineReturnedLinkPolicy({ - relation: 'download', + relation: 'Job Status', method: 'GET', - version: 'v3', - path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], - response: 'stream', - timeoutMs: 5_000, - maxResponseBytes: 4_096, + endpoint: getJob, preserveGatewayBasePath: true, }) const client = createOracleEpmClient({ instanceUrl: 'https://epm.example.com/gateway', accessToken: Buffer.from('u:p').toString('base64'), }) + expect(() => client.validateReturnedLink(policy, { - rel: 'download', - method: 'POST', - href: 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc', + rel, + method: 'GET', + href: 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/jobs/42', }) - ).toThrow() + ).toThrow(OracleEpmError) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() }) it('rejects forged validated-link handles', async () => { diff --git a/apps/sim/lib/internal/oracle-epm/links.test.ts b/apps/sim/lib/internal/oracle-epm/links.test.ts index 2efbacb9ecd..5071412db8a 100644 --- a/apps/sim/lib/internal/oracle-epm/links.test.ts +++ b/apps/sim/lib/internal/oracle-epm/links.test.ts @@ -11,8 +11,8 @@ const routes = defineOracleEpmRouteSpace({ }) describe('Oracle EPM returned-link declarations', () => { - it('binds a frozen policy to an endpoint', () => { - const endpoint = routes.defineEndpoint({ + describe.each(['endpoint', 'route'] as const)('%s-bound policies', (binding) => { + const declaration = { method: 'GET', version: 'v3', path: [oracleEpmLiteral('download')], @@ -20,18 +20,88 @@ describe('Oracle EPM returned-link declarations', () => { response: 'stream', timeoutMs: 2_000, maxResponseBytes: 1_024, + } as const + const endpoint = routes.defineEndpoint(declaration) + + function definePolicy(relation: string) { + return routes.defineReturnedLinkPolicy({ + relation, + method: 'GET', + ...(binding === 'endpoint' + ? { endpoint } + : { + version: declaration.version, + path: declaration.path, + response: declaration.response, + timeoutMs: declaration.timeoutMs, + maxResponseBytes: declaration.maxResponseBytes, + }), + preserveGatewayBasePath: true, + }) + } + + it.each([ + 'a', + 'download', + 'report-content.v1_2', + 'Job Status', + 'Download link', + 'Report Job Status', + 'Job 1.v2_3-4', + 'a'.repeat(64), + `Job ${'a'.repeat(60)}`, + ])('preserves relation %j in a frozen policy', (relation) => { + const policy = definePolicy(relation) + const definition = getOracleEpmReturnedLinkPolicy(policy) + expect(Object.isFrozen(policy)).toBe(true) + expect(Object.isFrozen(definition)).toBe(true) + expect(definition).toMatchObject({ relation, method: 'GET', version: 'v3' }) }) - const policy = routes.defineReturnedLinkPolicy({ - relation: 'download', - method: 'GET', - endpoint, - preserveGatewayBasePath: true, + + it.each([ + '', + 'a'.repeat(65), + `Job ${'a'.repeat(61)}`, + '1Job', + '.Job', + '_Job', + '-Job', + ' Job Status', + 'Job Status ', + 'Job Status', + 'Job\tStatus', + 'Job\nStatus', + 'Job\rStatus', + 'download\n', + 'download\r', + 'Job Status\n', + 'Job Status\r\n', + 'Job\u0000Status', + 'Job\u001fStatus', + 'Job\u007fStatus', + 'Job\u00a0Status', + 'Job\u200bStatus', + 'Job Status\u2028', + 'Job Status\u2029', + 'Job/Status', + 'Job\\Status', + 'Job:Status', + 'Job%20Status', + 'Jób Status', + 'Job 😀', + 'Job\uD800', + ])('rejects invalid relation %j', (relation) => { + expect(() => definePolicy(relation)).toThrow('Oracle EPM returned-link relation is invalid') }) - expect(Object.isFrozen(policy)).toBe(true) - expect(getOracleEpmReturnedLinkPolicy(policy)).toMatchObject({ - relation: 'download', - method: 'GET', - version: 'v3', + + it.each( + [undefined, null, 1, true, {}, ['Job Status'], { toString: () => 'Job Status' }].map( + (relation) => ({ relation }) + ) + )('rejects non-string relation $relation without coercion', ({ relation }) => { + expect(() => definePolicy(relation as unknown as string)).toThrow( + 'Oracle EPM returned-link relation is invalid' + ) }) }) diff --git a/apps/sim/lib/internal/oracle-epm/links.ts b/apps/sim/lib/internal/oracle-epm/links.ts index 401d5533f1e..02abc709f14 100644 --- a/apps/sim/lib/internal/oracle-epm/links.ts +++ b/apps/sim/lib/internal/oracle-epm/links.ts @@ -12,7 +12,8 @@ import type { } from '@/lib/internal/oracle-epm/types' const policies = new WeakMap() -const RELATION = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/ +/** Single ASCII spaces separate words; the final assertion rejects trailing line breaks too. */ +const RELATION = /^[A-Za-z][A-Za-z0-9._-]*(?: [A-Za-z0-9._-]+)*(?![\s\S])/ /** Internal frozen link policy available only after runtime-brand validation. */ export interface OracleEpmReturnedLinkPolicyDefinition { @@ -32,7 +33,11 @@ export function defineOracleEpmReturnedLinkPolicy( declaration: OracleEpmReturnedLinkPolicyDeclaration ): OracleEpmReturnedLinkPolicy { const route = getOracleEpmRouteSpace(routeSpace) - if (!RELATION.test(declaration.relation)) + if ( + typeof declaration.relation !== 'string' || + declaration.relation.length > 64 || + !RELATION.test(declaration.relation) + ) throw new Error('Oracle EPM returned-link relation is invalid') if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].includes(declaration.method)) throw new Error('Oracle EPM returned-link method is invalid') From 8c5f26c1e8d80135d7feb7e99d62b8446b7a6b28 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 15:39:29 -0700 Subject: [PATCH 14/20] test(oracle-epm): cover credential setup guidance --- .../client-credential-accounts/descriptors.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/sim/lib/credentials/client-credential-accounts/descriptors.test.ts b/apps/sim/lib/credentials/client-credential-accounts/descriptors.test.ts index ca6400765c8..91ac6705fbf 100644 --- a/apps/sim/lib/credentials/client-credential-accounts/descriptors.test.ts +++ b/apps/sim/lib/credentials/client-credential-accounts/descriptors.test.ts @@ -61,6 +61,20 @@ describe('partitionClientCredentialFields', () => { expect(oracleEpm.connectNoun).toBe('integration user') expect(oracleEpm.fields.find((field) => field.id === 'clientSecret')?.secret).toBe(true) }) + + it('guides Oracle EPM users to the REST base URL and authentication docs', () => { + const restBaseUrl = oracleEpm.fields.find((field) => field.id === 'orgId') + + expect(restBaseUrl).toMatchObject({ + label: 'REST Base URL', + placeholder: 'https://example.oraclecloud.com', + }) + expect(restBaseUrl?.hint).toContain('without /epmcloud') + expect(restBaseUrl?.hint).toContain('gateway prefix') + expect(oracleEpm.docsUrl).toBe( + 'https://docs.oracle.com/en/cloud/saas/enterprise-performance-management-common/prest/authentication.html' + ) + }) }) describe('Salesforce, which offers two grants', () => { From 7840639d64c229b195ac94a927e20d8e1b7558ee Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 16:20:42 -0700 Subject: [PATCH 15/20] fix(oracle-epm): bound DNS waits and validate header values --- .../internal/oracle-epm/client.server.test.ts | 129 +++++++++++++++++- .../lib/internal/oracle-epm/client.server.ts | 44 +++++- 2 files changed, 166 insertions(+), 7 deletions(-) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index 5c4bb78f088..233b8df42c0 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -1,5 +1,6 @@ /** @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { AsyncValidationResult } from '@/lib/core/security/input-validation.server' import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' const { mockSecureFetch, mockValidateUrl } = vi.hoisted(() => ({ @@ -139,6 +140,41 @@ describe('Oracle EPM guarded client', () => { expect(mockSecureFetch.mock.calls[0][0]).toContain('%252e%252e%252fadmin') }) + it.each([null, 123, true, {}, ['etag'], new Uint8Array([65])])( + 'rejects non-string header %j before DNS or fetch', + async (etag) => { + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await expect( + client.request(getJob, { + pathParams: { jobId: '42' }, + headers: { etag: etag as unknown as string }, + }) + ).rejects.toMatchObject({ name: 'OracleEpmError', category: 'invalid_input' }) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + } + ) + + it('rejects header objects without invoking their string coercion', async () => { + const stringifyHeader = vi.fn(() => 'coerced-header') + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + await expect( + client.request(getJob, { + pathParams: { jobId: '42' }, + headers: { etag: { toString: stringifyHeader } as unknown as string }, + }) + ).rejects.toMatchObject({ category: 'invalid_input' }) + expect(stringifyHeader).not.toHaveBeenCalled() + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + it('rejects malformed UTF-16 path input before URL encoding', async () => { const client = createOracleEpmClient({ instanceUrl: 'https://epm.example.com', @@ -305,9 +341,100 @@ describe('Oracle EPM guarded client', () => { signal: controller.signal, }) ).rejects.toMatchObject({ name: 'AbortError' }) + expect(mockValidateUrl).not.toHaveBeenCalled() expect(mockSecureFetch).not.toHaveBeenCalled() }) + describe('DNS cancellation', () => { + afterEach(() => vi.restoreAllMocks()) + + it.each(['deadline', 'caller'] as const)( + 'ends on %s cancellation even when DNS never settles', + async (source) => { + const deadline = new AbortController() + const caller = new AbortController() + const timeout = vi.spyOn(AbortSignal, 'timeout').mockReturnValue(deadline.signal) + mockValidateUrl.mockReturnValueOnce(new Promise(() => {})) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + const rejected = vi.fn() + const fulfilled = vi.fn() + const request = client + .request(getJob, { pathParams: { jobId: '42' }, signal: caller.signal }) + .then(fulfilled, rejected) + const callerReason = new DOMException('caller cancelled', 'AbortError') + if (source === 'deadline') deadline.abort(new DOMException('deadline', 'TimeoutError')) + else caller.abort(callerReason) + + await vi.waitFor(() => expect(rejected).toHaveBeenCalledTimes(1), { + interval: 1, + timeout: 100, + }) + await request + expect(timeout).toHaveBeenCalledWith(5_000) + expect(rejected).toHaveBeenCalledWith( + source === 'deadline' + ? expect.objectContaining({ category: 'timeout', retryable: true }) + : callerReason + ) + expect(fulfilled).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + } + ) + + it.each(['resolve', 'reject'] as const)( + 'does not revive a cancelled request when DNS later %ss', + async (settlement) => { + const dns = Promise.withResolvers() + mockValidateUrl.mockReturnValueOnce(dns.promise) + const controller = new AbortController() + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + const rejected = vi.fn() + const request = client + .request(getJob, { pathParams: { jobId: '42' }, signal: controller.signal }) + .catch(rejected) + controller.abort(new DOMException('caller cancelled', 'AbortError')) + await vi.waitFor(() => expect(rejected).toHaveBeenCalledTimes(1), { + interval: 1, + timeout: 100, + }) + await request + + if (settlement === 'resolve') { + dns.resolve({ + isValid: true, + resolvedIP: '203.0.113.10', + originalHostname: 'epm.example.com', + }) + } else { + dns.reject(new Error('late resolver failure')) + } + await Promise.resolve() + expect(rejected).toHaveBeenCalledTimes(1) + expect(mockSecureFetch).not.toHaveBeenCalled() + } + ) + + it('suppresses unexpected DNS rejection details', async () => { + mockValidateUrl.mockRejectedValueOnce(new Error('private resolver failure')) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com', + accessToken: Buffer.from('u:p').toString('base64'), + }) + const error = await client + .request(getJob, { pathParams: { jobId: '42' } }) + .catch((value: unknown) => value) + expect(error).toMatchObject({ category: 'service_unavailable', retryable: true }) + expect(String(error)).not.toContain('private resolver failure') + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + }) + it.each(['download', 'Job Status'])( 'keeps %s links opaque and client-owned', async (relation) => { diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts index d06160a0b05..1bc7050b6fc 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -1,6 +1,7 @@ import { interruptibleSleep } from '@sim/utils/helpers' import { backoffWithJitter } from '@sim/utils/retry' import { + type AsyncValidationResult, type SecureFetchResponse, secureFetchWithPinnedIP, validateUrlWithDNS, @@ -168,6 +169,7 @@ function buildHeaders( continue } if ( + typeof value !== 'string' || /\r|\n|\u0000/.test(value) || MALFORMED_UTF16.test(value) || Buffer.byteLength(value, 'utf8') > declaration.maxBytes || @@ -402,6 +404,34 @@ export interface OracleEpmClient { ): Promise } +/** Bounds the DNS wait without changing the platform resolver's own lifetime. */ +function validateDestinationWithSignal( + origin: string, + signal: AbortSignal +): Promise { + signal.throwIfAborted() + return new Promise((resolve, reject) => { + const cleanup = () => signal.removeEventListener('abort', onAbort) + const onAbort = () => { + cleanup() + reject(signal.reason) + } + signal.addEventListener('abort', onAbort, { once: true }) + validateUrlWithDNS(origin, 'Oracle EPM destination', 'configuredEndpoint', { + logDetails: false, + }).then( + (validation) => { + cleanup() + resolve(validation) + }, + (error: unknown) => { + cleanup() + reject(error) + } + ) + }) +} + /** Creates a fixed-destination Oracle EPM client from resolved credential material. */ export function createOracleEpmClient(input: { instanceUrl: string @@ -430,12 +460,14 @@ export function createOracleEpmClient(input: { const signal = request.signal ? AbortSignal.any([request.signal, deadlineSignal]) : deadlineSignal - const validation = await validateUrlWithDNS( - destinationData.origin, - 'Oracle EPM destination', - 'configuredEndpoint', - { logDetails: false } - ) + let validation: AsyncValidationResult + try { + validation = await validateDestinationWithSignal(destinationData.origin, signal) + } catch (error) { + if (request.signal?.aborted) throw request.signal.reason ?? error + if (deadlineSignal.aborted) throw oracleEpmLocalError('timeout', true) + throw oracleEpmLocalError('service_unavailable', true) + } if (request.signal?.aborted) { throw request.signal.reason ?? new DOMException('Aborted', 'AbortError') } From fbb5a4edc72fc403d102a9d96474a66124e75f0d Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 16:31:17 -0700 Subject: [PATCH 16/20] fix(oracle-epm): reject malformed returned-link entries --- .../internal/oracle-epm/client.server.test.ts | 25 +++++++++++++++++++ .../lib/internal/oracle-epm/client.server.ts | 2 ++ 2 files changed, 27 insertions(+) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index 233b8df42c0..5cd8357ee94 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -435,6 +435,31 @@ describe('Oracle EPM guarded client', () => { }) }) + it.each([null, undefined, 'invalid-link', 123, true, [], {}, { rel: 'download' }])( + 'rejects malformed returned-link entry %j with a safe error', + (entry) => { + const policy = routes.defineReturnedLinkPolicy({ + relation: 'download', + method: 'GET', + endpoint: getJob, + preserveGatewayBasePath: true, + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('u:p').toString('base64'), + }) + + expect(() => + client.validateReturnedLink( + policy, + entry as unknown as Parameters[1] + ) + ).toThrowError(expect.objectContaining({ name: 'OracleEpmError', category: 'invalid_input' })) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + } + ) + it.each(['download', 'Job Status'])( 'keeps %s links opaque and client-owned', async (relation) => { diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts index 1bc7050b6fc..388fe8a98ba 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -551,6 +551,8 @@ export function createOracleEpmClient(input: { ) { const policy = getOracleEpmReturnedLinkPolicy(policyValue) if ( + typeof link !== 'object' || + link === null || link.rel !== policy.relation || (link.method !== undefined && link.method !== policy.method) || typeof link.href !== 'string' || From 68df9971a2e9fe11dd2e9805dad5b265b6d853ff Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 16:40:17 -0700 Subject: [PATCH 17/20] fix(oracle-epm): validate typed returned-link queries --- .../internal/oracle-epm/client.server.test.ts | 90 +++++++++++++++++++ .../lib/internal/oracle-epm/client.server.ts | 12 ++- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index 5cd8357ee94..bca7caf56a5 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -460,6 +460,96 @@ describe('Oracle EPM guarded client', () => { } ) + describe.each(['endpoint', 'route'] as const)( + '%s-bound typed returned-link queries', + (binding) => { + const declaration = { + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('jobs')], + query: { + offset: oracleEpmQuery.integer({ required: true, minimum: -10, maximum: 10 }), + include: oracleEpmQuery.boolean({ required: true }), + token: oracleEpmQuery.string({ required: true, maxBytes: 32 }), + }, + body: 'none', + response: 'json', + timeoutMs: 5_000, + maxResponseBytes: 4_096, + } as const + const endpoint = routes.defineEndpoint(declaration) + const policy = routes.defineReturnedLinkPolicy({ + ...(binding === 'endpoint' ? { endpoint, method: 'GET' as const } : declaration), + relation: 'next', + preserveGatewayBasePath: true, + }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('u:p').toString('base64'), + }) + const prefix = 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/jobs' + + it.each(['offset=-10&include=false', 'offset=0&include=false', 'offset=10&include=true'])( + 'accepts canonical typed query %s without rewriting the returned URL', + async (query) => { + const href = `${prefix}?${query}&token=signed%2Bquery%2Fsecret` + const handle = client.validateReturnedLink(policy, { rel: 'next', href }) + expect(JSON.stringify(handle)).toBe('{}') + await client.requestValidatedLink(handle) + expect(mockSecureFetch.mock.calls[0][0]).toBe(href) + } + ) + + it.each([ + 'offset=-11&include=true', + 'offset=11&include=true', + 'offset=9007199254740992&include=true', + 'offset=1.5&include=true', + 'offset=1e0&include=true', + 'offset=0x1&include=true', + 'offset=01&include=true', + 'offset=-0&include=true', + 'offset=%201&include=true', + 'offset=%2B1&include=true', + 'offset=&include=true', + 'offset=NaN&include=true', + 'offset=Infinity&include=true', + 'offset=1&include=True', + 'offset=1&include=FALSE', + 'offset=1&include=1', + 'offset=1&include=0', + 'offset=1&include=', + 'offset=1', + 'include=true', + 'offset=1&offset=2&include=true', + 'offset=1&include=true&include=false', + 'offset=1&include=true&unknown=value', + ])('rejects invalid typed query %s before DNS or fetch', (query) => { + expect(() => + client.validateReturnedLink(policy, { + rel: 'next', + href: `${prefix}?${query}&token=signed-query-secret`, + }) + ).toThrowError( + expect.objectContaining({ name: 'OracleEpmError', category: 'invalid_input' }) + ) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + + it.each([ + { offset: '1', include: true }, + { offset: 1, include: 'true' }, + ])('keeps direct request query types strict for %j', async (query) => { + await expect( + client.request(endpoint, { query: { ...query, token: 'signed-query-secret' } }) + ).rejects.toMatchObject({ category: 'invalid_input' }) + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + }) + } + ) + it.each(['download', 'Job Status'])( 'keeps %s links opaque and client-owned', async (relation) => { diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts index 388fe8a98ba..16b98419fdf 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -132,6 +132,16 @@ function serializeQueryValue(value: unknown, declaration: OracleEpmQueryParamete return String(value) } +/** Accepts only the canonical wire form of each declared type without rewriting the URL. */ +function validateReturnedQueryValue(value: string, declaration: OracleEpmQueryParameter): void { + let parsed: string | number | boolean = value + if (declaration.kind === 'integer') parsed = Number(value) + else if (declaration.kind === 'boolean') parsed = value === 'true' + if (serializeQueryValue(parsed, declaration) !== value) { + throw oracleEpmLocalError('invalid_input') + } +} + function buildQuery( declarations: Readonly>, values: Readonly> | undefined @@ -589,7 +599,7 @@ export function createOracleEpmClient(input: { if (seen.has(name) || !Object.hasOwn(policy.query, name)) throw oracleEpmLocalError('invalid_input') seen.add(name) - serializeQueryValue(value, policy.query[name]) + validateReturnedQueryValue(value, policy.query[name]) } for (const [name, declaration] of Object.entries(policy.query)) { if (declaration.required && !seen.has(name)) throw oracleEpmLocalError('invalid_input') From 9ec1be01aac6fac1bbacafe14e93f53188f45c1b Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 16:47:44 -0700 Subject: [PATCH 18/20] fix(oracle-epm): reject raw backslashes before link parsing --- apps/sim/lib/internal/oracle-epm/client.server.test.ts | 10 ++++++++-- apps/sim/lib/internal/oracle-epm/client.server.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index bca7caf56a5..7727f65bcc0 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -492,7 +492,7 @@ describe('Oracle EPM guarded client', () => { it.each(['offset=-10&include=false', 'offset=0&include=false', 'offset=10&include=true'])( 'accepts canonical typed query %s without rewriting the returned URL', async (query) => { - const href = `${prefix}?${query}&token=signed%2Bquery%2Fsecret` + const href = `${prefix}?${query}&token=signed%2Bquery%2Fsecret%5Cvalue` const handle = client.validateReturnedLink(policy, { rel: 'next', href }) expect(JSON.stringify(handle)).toBe('{}') await client.requestValidatedLink(handle) @@ -605,12 +605,16 @@ describe('Oracle EPM guarded client', () => { it.each( [ 'https://evil.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://epm.example.com\\injected/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://epm.example.com:443\\injected/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', + 'https://epm.example.com\\..\\injected/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', 'https://user@epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x', 'https://epm.example.com/SyntheticAlpha/rest/v3/files/abc?token=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x&token=y', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?unknown=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#fragment', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#', + 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x\\y', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/ab\nc?token=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/\uD800?token=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest//v3/files/abc?token=x', @@ -639,7 +643,9 @@ describe('Oracle EPM guarded client', () => { instanceUrl: 'https://epm.example.com/gateway', accessToken: Buffer.from('u:p').toString('base64'), }) - expect(() => client.validateReturnedLink(policy, { rel: relation, href })).toThrow() + expect(() => client.validateReturnedLink(policy, { rel: relation, href })).toThrowError( + expect.objectContaining({ name: 'OracleEpmError', category: 'invalid_input' }) + ) expect(mockValidateUrl).not.toHaveBeenCalled() expect(mockSecureFetch).not.toHaveBeenCalled() }) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.ts b/apps/sim/lib/internal/oracle-epm/client.server.ts index 16b98419fdf..d0024d3047a 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.ts @@ -369,10 +369,10 @@ function decodeReturnedPathSegment( } function rawReturnedPathSegments(href: string): string[] { + if (href.includes('\\')) throw oracleEpmLocalError('invalid_input') const match = /^https:\/\/[^/?#]*(\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i.exec(href) if (!match) throw oracleEpmLocalError('invalid_input') const rawPath = match[1] ?? '' - if (rawPath.includes('\\')) throw oracleEpmLocalError('invalid_input') if (!rawPath) return [] const segments = rawPath.slice(1).split('/') if (segments.some((segment) => !segment)) throw oracleEpmLocalError('invalid_input') From 9e738a3b021bbfb2c1c065ee1a7f241a4c41fdfc Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Fri, 4 Sep 2026 19:54:16 -0700 Subject: [PATCH 19/20] fix(oracle-epm): support slash-containing link relations --- .../internal/oracle-epm/client.server.test.ts | 95 ++++++++++++------- .../sim/lib/internal/oracle-epm/links.test.ts | 9 +- apps/sim/lib/internal/oracle-epm/links.ts | 4 +- 3 files changed, 69 insertions(+), 39 deletions(-) diff --git a/apps/sim/lib/internal/oracle-epm/client.server.test.ts b/apps/sim/lib/internal/oracle-epm/client.server.test.ts index 7727f65bcc0..efe64e71513 100644 --- a/apps/sim/lib/internal/oracle-epm/client.server.test.ts +++ b/apps/sim/lib/internal/oracle-epm/client.server.test.ts @@ -550,7 +550,7 @@ describe('Oracle EPM guarded client', () => { } ) - it.each(['download', 'Job Status'])( + it.each(['download', 'Job Status', 'exported/artifact', 'imported/artifact'])( 'keeps %s links opaque and client-owned', async (relation) => { const download = routes.defineEndpoint({ @@ -626,7 +626,12 @@ describe('Oracle EPM guarded client', () => { 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%2Fabc?token=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%5Cabc?token=x', 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files\\abc?token=x', - ].flatMap((href) => ['download', 'Job Status'].map((relation) => ({ relation, href }))) + ].flatMap((href) => + ['download', 'Job Status', 'exported/artifact', 'imported/artifact'].map((relation) => ({ + relation, + href, + })) + ) )('rejects unsafe $relation link $href', ({ relation, href }) => { const policy = routes.defineReturnedLinkPolicy({ relation, @@ -650,44 +655,62 @@ describe('Oracle EPM guarded client', () => { expect(mockSecureFetch).not.toHaveBeenCalled() }) - it.each(['download', 'Job Status'])('rejects an incorrect %s link method', (relation) => { - const policy = routes.defineReturnedLinkPolicy({ - relation, - method: 'GET', - version: 'v3', - path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], - response: 'stream', - timeoutMs: 5_000, - maxResponseBytes: 4_096, - preserveGatewayBasePath: true, - }) - const client = createOracleEpmClient({ - instanceUrl: 'https://epm.example.com/gateway', - accessToken: Buffer.from('u:p').toString('base64'), - }) - expect(() => - client.validateReturnedLink(policy, { - rel: relation, - method: 'POST', - href: 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc', + it.each(['download', 'Job Status', 'exported/artifact', 'imported/artifact'])( + 'rejects an incorrect %s link method', + (relation) => { + const policy = routes.defineReturnedLinkPolicy({ + relation, + method: 'GET', + version: 'v3', + path: [oracleEpmLiteral('files'), oracleEpmPathParameter('fileId', { maxBytes: 32 })], + response: 'stream', + timeoutMs: 5_000, + maxResponseBytes: 4_096, + preserveGatewayBasePath: true, }) - ).toThrow() - expect(mockValidateUrl).not.toHaveBeenCalled() - expect(mockSecureFetch).not.toHaveBeenCalled() - }) + const client = createOracleEpmClient({ + instanceUrl: 'https://epm.example.com/gateway', + accessToken: Buffer.from('u:p').toString('base64'), + }) + expect(() => + client.validateReturnedLink(policy, { + rel: relation, + method: 'POST', + href: 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc', + }) + ).toThrow() + expect(mockValidateUrl).not.toHaveBeenCalled() + expect(mockSecureFetch).not.toHaveBeenCalled() + } + ) it.each([ - 'job status', - 'Job status', - ' Job Status', - 'Job Status ', - 'Job Status', - 'Job\tStatus', - 'Job Status\n', - 'download', - ])('rejects nonmatching relation %j before DNS or network access', (rel) => { + ...[ + 'job status', + 'Job status', + ' Job Status', + 'Job Status ', + 'Job Status', + 'Job\tStatus', + 'Job Status\n', + 'download', + ].map((rel) => ({ relation: 'Job Status', rel })), + ...[ + 'Exported/artifact', + 'exported/Artifact', + ' exported/artifact', + 'exported/artifact ', + 'exported/artifact\n', + 'exported%2Fartifact', + 'exported\\artifact', + 'exported//artifact', + 'exported/artifact/extra', + 'imported/artifact', + 'self', + ].map((rel) => ({ relation: 'exported/artifact', rel })), + ])('rejects $rel for $relation before DNS or network access', ({ relation, rel }) => { const policy = routes.defineReturnedLinkPolicy({ - relation: 'Job Status', + relation, method: 'GET', endpoint: getJob, preserveGatewayBasePath: true, diff --git a/apps/sim/lib/internal/oracle-epm/links.test.ts b/apps/sim/lib/internal/oracle-epm/links.test.ts index 5071412db8a..6944d5be968 100644 --- a/apps/sim/lib/internal/oracle-epm/links.test.ts +++ b/apps/sim/lib/internal/oracle-epm/links.test.ts @@ -44,12 +44,16 @@ describe('Oracle EPM returned-link declarations', () => { 'a', 'download', 'report-content.v1_2', + 'exported/artifact', + 'imported/artifact', + 'Job/Status', 'Job Status', 'Download link', 'Report Job Status', 'Job 1.v2_3-4', 'a'.repeat(64), `Job ${'a'.repeat(60)}`, + `exported/${'a'.repeat(55)}`, ])('preserves relation %j in a frozen policy', (relation) => { const policy = definePolicy(relation) const definition = getOracleEpmReturnedLinkPolicy(policy) @@ -62,10 +66,12 @@ describe('Oracle EPM returned-link declarations', () => { '', 'a'.repeat(65), `Job ${'a'.repeat(61)}`, + `exported/${'a'.repeat(56)}`, '1Job', '.Job', '_Job', '-Job', + '/artifact', ' Job Status', 'Job Status ', 'Job Status', @@ -83,8 +89,9 @@ describe('Oracle EPM returned-link declarations', () => { 'Job\u200bStatus', 'Job Status\u2028', 'Job Status\u2029', - 'Job/Status', 'Job\\Status', + 'exported/artifact\n', + 'exported%2Fartifact', 'Job:Status', 'Job%20Status', 'Jób Status', diff --git a/apps/sim/lib/internal/oracle-epm/links.ts b/apps/sim/lib/internal/oracle-epm/links.ts index 02abc709f14..c4f2cda94a8 100644 --- a/apps/sim/lib/internal/oracle-epm/links.ts +++ b/apps/sim/lib/internal/oracle-epm/links.ts @@ -12,8 +12,8 @@ import type { } from '@/lib/internal/oracle-epm/types' const policies = new WeakMap() -/** Single ASCII spaces separate words; the final assertion rejects trailing line breaks too. */ -const RELATION = /^[A-Za-z][A-Za-z0-9._-]*(?: [A-Za-z0-9._-]+)*(?![\s\S])/ +/** Slashes are literal relation characters; single ASCII spaces separate words. */ +const RELATION = /^[A-Za-z][A-Za-z0-9._/-]*(?: [A-Za-z0-9._/-]+)*(?![\s\S])/ /** Internal frozen link policy available only after runtime-brand validation. */ export interface OracleEpmReturnedLinkPolicyDefinition { From 4e18e8ee9f006f145c202b74837955dbd7328f7a Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Sat, 5 Sep 2026 16:41:34 -0700 Subject: [PATCH 20/20] feat(oracle-epm): add Profitability and Cost Management integration --- apps/docs/components/ui/icon-mapping.ts | 1 + apps/docs/content/docs/integrations/meta.json | 1 + .../integrations/oracle_epm_profitability.mdx | 511 +++++++++ .../blocks/blocks/oracle_epm_profitability.ts | 995 ++++++++++++++++++ apps/sim/blocks/registry-maps.ts | 3 + .../lib/copilot/generated/docs-manifest.ts | 1 + .../integrations/credential-display.test.ts | 23 + apps/sim/lib/integrations/icon-mapping.ts | 1 + .../execute-tool.test.ts | 125 +++ .../oracle-epm-profitability/execute-tool.ts | 64 ++ .../files.server.test.ts | 300 ++++++ .../oracle-epm-profitability/files.server.ts | 190 ++++ .../oracle-epm-profitability/jobs.test.ts | 138 +++ .../internal/oracle-epm-profitability/jobs.ts | 88 ++ .../oracle-epm-profitability/normalizers.ts | 122 +++ .../operations.test.ts | 318 ++++++ .../oracle-epm-profitability/operations.ts | 416 ++++++++ .../tool-operations/registry.server.ts | 26 + apps/sim/lib/oauth/oauth.ts | 18 + apps/sim/lib/oauth/types.ts | 1 + apps/sim/lib/selectors/manifest.test.ts | 24 +- apps/sim/lib/selectors/manifest.ts | 2 + .../oracle-epm-profitability.test.ts | 155 +++ .../providers/oracle-epm-profitability.ts | 115 ++ apps/sim/lib/selectors/server/registry.ts | 2 + apps/sim/tools/generated/tool-ids.ts | 2 +- apps/sim/tools/generated/tool-metadata.ts | 2 +- apps/sim/tools/generated/tool-outputs.ts | 2 +- .../apply_data_grants.ts | 31 + .../oracle_epm_profitability/clear_pov.ts | 76 ++ .../oracle_epm_profitability/copy_pov.ts | 81 ++ .../create_application.ts | 49 + .../oracle_epm_profitability/deploy_cube.ts | 51 + .../oracle_epm_profitability/download_file.ts | 32 + .../enable_application.ts | 31 + .../export_query_results.ts | 100 ++ .../generate_program_documentation.ts | 84 ++ .../get_rule_balancing.ts | 50 + .../get_task_status.ts | 31 + .../import_template.ts | 51 + .../tools/oracle_epm_profitability/index.ts | 21 + .../oracle_epm_profitability/list_files.ts | 25 + .../oracle_epm_profitability/load_data.ts | 51 + .../oracle_epm_profitability/merge_slices.ts | 38 + .../oracle_epm_profitability/optimize_cube.ts | 38 + .../run_calculation.ts | 102 ++ .../tools/oracle_epm_profitability/types.ts | 298 ++++++ .../update_dimensions.ts | 51 + .../oracle_epm_profitability/upload_file.ts | 38 + .../tools/oracle_epm_profitability/utils.ts | 37 + .../oracle_epm_profitability/wait_for_task.ts | 38 + apps/sim/tools/registry.ts | 43 + .../deployment-config/src/integrations.json | 101 +- 53 files changed, 5186 insertions(+), 8 deletions(-) create mode 100644 apps/docs/content/docs/integrations/oracle_epm_profitability.mdx create mode 100644 apps/sim/blocks/blocks/oracle_epm_profitability.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/execute-tool.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/execute-tool.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/files.server.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/files.server.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/jobs.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/jobs.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/normalizers.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/operations.test.ts create mode 100644 apps/sim/lib/internal/oracle-epm-profitability/operations.ts create mode 100644 apps/sim/lib/selectors/server/providers/oracle-epm-profitability.test.ts create mode 100644 apps/sim/lib/selectors/server/providers/oracle-epm-profitability.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/apply_data_grants.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/clear_pov.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/copy_pov.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/create_application.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/deploy_cube.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/download_file.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/enable_application.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/export_query_results.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/generate_program_documentation.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/get_rule_balancing.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/get_task_status.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/import_template.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/index.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/list_files.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/load_data.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/merge_slices.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/optimize_cube.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/run_calculation.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/types.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/update_dimensions.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/upload_file.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/utils.ts create mode 100644 apps/sim/tools/oracle_epm_profitability/wait_for_task.ts diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 032f0756383..8b13f9fbe52 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -482,6 +482,7 @@ export const blockTypeToIconMap: Record = { onedrive: MicrosoftOneDriveIcon, onepassword: OnePasswordIcon, openai: OpenAIIcon, + oracle_epm_profitability: NetSuiteIcon, outlook: OutlookIcon, pagerduty: PagerDutyIcon, parallel_ai: ParallelIcon, diff --git a/apps/docs/content/docs/integrations/meta.json b/apps/docs/content/docs/integrations/meta.json index 69d1dac6bfc..c0d63b14003 100644 --- a/apps/docs/content/docs/integrations/meta.json +++ b/apps/docs/content/docs/integrations/meta.json @@ -188,6 +188,7 @@ "onedrive", "onepassword", "openai", + "oracle_epm_profitability", "outlook", "pagerduty", "parallel_ai", diff --git a/apps/docs/content/docs/integrations/oracle_epm_profitability.mdx b/apps/docs/content/docs/integrations/oracle_epm_profitability.mdx new file mode 100644 index 00000000000..0d4037b27ad --- /dev/null +++ b/apps/docs/content/docs/integrations/oracle_epm_profitability.mdx @@ -0,0 +1,511 @@ +--- +title: Oracle EPM Profitability and Cost Management (PCM) +description: Calculate allocations and manage PCM data, POVs, reports, and tasks +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Automate Cloud PCM Management Ledger applications using the epm/rest/v1 API family. This is distinct from Enterprise Profitability and Cost Management (EPCM). Use a provisioned PCM environment under an eligible EPM Enterprise or legacy PCM subscription. Most operations require Service Administrator or Power User; creating an application requires Service Administrator. Report and query-export submissions accept all application roles, but task polling requires Service Administrator or Power User. File operations and file selectors require Service Administrator or an application role with Migrations - Administer. Upload inputs to profitinbox and download reports from profitoutbox. Downloads require repository paths of at most 255 UTF-8 bytes; use short output filenames. Submission and waiting are separate; retain processName and inspect the Job Library after ambiguous failures. Application, POV, model view, rule, and query names are manual inputs. Template export and undocumented nested rule-balancing fields are not exposed. + + + +## Actions + +### Oracle PCM Create Application + +Create a PCM file-based application with rule and balance dimensions. Requires Service Administrator. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `description` | string | Yes | Description of the application | +| `ruleDimensionName` | string | Yes | Rule dimension name | +| `balanceDimensionName` | string | Yes | Balance dimension name | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Enable Application + +Enable a PCM file-based application after loading its dimensions. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Deploy Cube + +Deploy or redeploy the PCM calculation cube immediately. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `isKeepData` | boolean | Yes | Preserve existing cube data; cannot combine with isReplaceCube | +| `isReplaceCube` | boolean | Yes | Replace the existing cube; cannot combine with isKeepData | +| `comment` | string | Yes | Comment for the operation | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Update Dimensions + +Update PCM dimensions from metadata files already staged in profitinbox. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `dataFileName` | string | Yes | Filename already staged in profitinbox; dimension updates accept a delimiter-separated list | +| `stringDelimiter` | string | No | Single separator: defaults to underscore for POVs and comma for dimension file lists | +| `acceptableDecreasePercentage` | number | No | Maximum permitted dimension member decrease percentage, 0 to 100 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Load Data + +Load Essbase input data from a file in profitinbox. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `clearAllDataFlag` | boolean | Yes | Clear all existing data before loading | +| `dataLoadValue` | string | Yes | How incoming values affect existing data Allowed values: ADD_EXISTING_VALUES, OVERWRITE_EXISTING_VALUES. | +| `dataFileName` | string | Yes | Filename already staged in profitinbox; dimension updates accept a delimiter-separated list | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Run Calculation + +Run PCM allocations using all rules, a rule-set range, or one rule. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `povName` | string | Yes | POV members joined by stringDelimiter; model POV for calculations | +| `exeType` | string | Yes | Rules to execute; dataPOVName requires ALL_RULES Allowed values: ALL_RULES, RULESET_SUBSET, SINGLE_RULE. | +| `dataPOVName` | string | No | Optional different data POV; requires ALL_RULES | +| `isClearCalculated` | boolean | No | Clear previously calculated data | +| `optimizeReporting` | boolean | No | Run default aggregation after calculation | +| `subsetStart` | number | No | First rule-set sequence; required for RULESET_SUBSET | +| `subsetEnd` | number | No | Last rule-set sequence; required for RULESET_SUBSET | +| `ruleName` | string | No | Existing rule name; required for SINGLE_RULE | +| `ruleSetName` | string | No | Existing rule set name; required for SINGLE_RULE | +| `comment` | string | No | Comment for the operation | +| `stringDelimiter` | string | No | Single separator: defaults to underscore for POVs and comma for dimension file lists | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Copy POV + +Copy PCM rules and input data to a destination POV. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `povName` | string | Yes | POV members joined by stringDelimiter; model POV for calculations | +| `destinationPovName` | string | Yes | Destination POV members joined by stringDelimiter | +| `isManageRule` | boolean | Yes | Copy or clear program rules for the selected POV | +| `isInputData` | boolean | Yes | Copy or clear input data for the selected POV | +| `modelViewName` | string | No | Existing model view to limit the data slice | +| `createDestPOV` | boolean | Yes | Create the destination POV if it does not exist | +| `nonEmptyTupleEnabled` | boolean | No | Enable nonempty tuple optimization when copying data | +| `stringDelimiter` | string | Yes | Single separator: defaults to underscore for POVs and comma for dimension file lists | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Clear POV + +Clear selected PCM POV rules or data, optionally limiting input data by an existing query. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `povName` | string | Yes | POV members joined by stringDelimiter; model POV for calculations | +| `isManageRule` | boolean | No | Copy or clear program rules for the selected POV | +| `isInputData` | boolean | No | Copy or clear input data for the selected POV | +| `queryName` | string | No | Existing PCM query; exports with no query export all application data | +| `isAllocatedValues` | boolean | No | Clear allocated values; incompatible with queryName | +| `isAdjustmentValues` | boolean | No | Clear adjustment values; incompatible with queryName | +| `stringDelimiter` | string | No | Single separator: defaults to underscore for POVs and comma for dimension file lists | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Get Rule Balancing + +Read documented scalar rule-balancing results for a PCM POV and model view; nested rules are not exposed. Requires Service Administrator or Power User. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `povName` | string | Yes | POV members joined by stringDelimiter; model POV for calculations | +| `modelViewName` | string | Yes | Existing model view to limit the data slice | +| `stringDelimiter` | string | No | Single separator: defaults to underscore for POVs and comma for dimension file lists | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | number | status output from the tool | +| `items` | array | Documented scalar rule-balancing fields; nested rules are omitted | +| ↳ `ruleNumber` | string | ruleNumber output from the tool | +| ↳ `balanceTypeRule` | boolean | balanceTypeRule output from the tool | +| ↳ `scale` | number | scale output from the tool | +| ↳ `sequence` | number | sequence output from the tool | +| ↳ `name` | string | name output from the tool | +| ↳ `description` | string | description output from the tool | +| ↳ `runningBalance` | number | runningBalance output from the tool | +| ↳ `balance` | number | balance output from the tool | +| ↳ `allocationIn` | number | allocationIn output from the tool | +| ↳ `allocationOut` | number | allocationOut output from the tool | +| ↳ `adjustmentIn` | number | adjustmentIn output from the tool | +| ↳ `adjustmentOut` | number | adjustmentOut output from the tool | +| ↳ `input` | number | input output from the tool | +| ↳ `runningRemainder` | number | runningRemainder output from the tool | +| ↳ `remainder` | number | remainder output from the tool | +| ↳ `netChange` | number | netChange output from the tool | +| ↳ `offset` | number | offset output from the tool | + +### Oracle PCM Generate Program Documentation + +Submit a PCM program documentation report to profitoutbox. Available to all application roles; status polling requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `povName` | string | Yes | POV members joined by stringDelimiter; model POV for calculations | +| `fileName` | string | No | Filename without a folder for upload/import/export; download uses the listed profitoutbox path | +| `fileType` | string | No | Program documentation format Allowed values: PDF, XML, WORD, EXCEL, HTML. | +| `skipFilters` | boolean | No | Use level-0 member counts instead of resolving rule filters in documentation | +| `subsetStart` | number | No | First rule-set sequence; required for RULESET_SUBSET | +| `subsetEnd` | number | No | Last rule-set sequence; required for RULESET_SUBSET | +| `useAlias` | boolean | No | Use aliases in the program documentation report | +| `stringDelimiter` | string | No | Single separator: defaults to underscore for POVs and comma for dimension file lists | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Export Query Results + +Export an existing PCM query or application data to profitoutbox. Available to all application roles; status polling requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `fileName` | string | Yes | Filename without a folder for upload/import/export; download uses the listed profitoutbox path | +| `queryName` | string | No | Existing PCM query; exports with no query export all application data | +| `exportOnlyLevel0Flg` | boolean | No | Export only level-0 query data; ignored when exporting the whole application | +| `fileOutputOptions` | string | No | Export compression and text-file output Allowed values: ZIP_ONLY, ZIP_AND_TEXT, TEXT_ONLY. | +| `roundingPrecision` | number | No | Decimal places for named query exports; Oracle defaults to 2 | +| `dataFormat` | string | No | NATIVE exports query data; COLUMNAR exports application data and ignores queryName Allowed values: NATIVE, COLUMNAR. | +| `memberFilters` | string | No | JSON string mapping dimensions to level-0 member arrays; applies to COLUMNAR exports | +| `includeHeader` | boolean | No | Include dimension column headers for COLUMNAR exports | +| `delimiter` | string | No | Single column separator for COLUMNAR exports; Oracle defaults to space | +| `keepDuplicateMemberFormat` | boolean | No | Use qualified duplicate member names for COLUMNAR exports | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Import Template + +Import a PCM application template ZIP already staged in profitinbox. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `description` | string | Yes | Description of the application | +| `fileName` | string | Yes | Filename without a folder for upload/import/export; download uses the listed profitoutbox path | +| `isApplicationOverwrite` | boolean | Yes | Overwrite an application with the same name when importing | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Apply Data Grants + +Recreate Essbase data grants from the latest PCM application grants. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Merge Slices + +Merge PCM incremental data slices into the main database slice. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `removeZeroCells` | boolean | No | Remove zero-valued cells while merging slices | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Optimize Cube + +Control query tracking or create and clear aggregate views for a PCM ASO cube. Requires Service Administrator or Power User. Submission is separate from waiting; do not blindly retry an ambiguous failure. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `applicationName` | string | Yes | Exact PCM Management Ledger application name | +| `type` | string | Yes | ASO optimization operation Allowed values: clearAggregations, createAggregations, startQueryTracking, stopQueryTracking, createQBOAggregations. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Get Task Status + +Read PCM task status using its processName. Requires Service Administrator or Power User. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `processName` | string | Yes | Task processName returned by PCM submission; not a numeric EPCM job ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier from a validated Job Status link; retain for polling | +| `status` | number | PCM status: -1 pending, 0 success, any positive value failed | +| `state` | string | Normalized pending, succeeded, or failed state | +| `statusMessage` | string | Oracle status message | +| `details` | string | Oracle task details; may be prose rather than an identifier | + +### Oracle PCM Wait for Task + +Wait within a bounded deadline for a PCM task; retain processName to resume after timeout. Requires Service Administrator or Power User. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `processName` | string | Yes | Task processName returned by PCM submission; not a numeric EPCM job ID | +| `maxWaitSeconds` | number | No | Maximum wait in seconds, 1 to 3600; does not cancel the remote task | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `processName` | string | Task identifier retained even when the wait expires | +| `timedOut` | boolean | Whether the bounded wait expired; the remote task was not cancelled | +| `attempts` | number | Status reads before a terminal result | + +### Oracle PCM Upload File + +Upload one authorized Sim file to profitinbox, up to 100 MiB. Existing names are rejected. Requires Service Administrator or an application role with Migrations - Administer. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `fileName` | string | Yes | Filename without a folder for upload/import/export; download uses the listed profitoutbox path | +| `file` | file | Yes | One canonical Sim UserFile or one-element upload array | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fileName` | string | Uploaded repository path in profitinbox | +| `status` | number | Zero means the ordinary-file upload succeeded | + +### Oracle PCM List Files + +List ordinary files in profitinbox and profitoutbox, excluding LCM snapshots. Requires Service Administrator or an application role with Migrations - Administer. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `files` | array | Ordinary PCM repository files; names include profitinbox or profitoutbox | +| ↳ `name` | string | name output from the tool | +| ↳ `type` | string | type output from the tool | +| ↳ `size` | number | size output from the tool | +| ↳ `lastModifiedTime` | number | lastModifiedTime output from the tool | + +### Oracle PCM Download File + +Download one listed PCM repository file as a Sim UserFile, up to 100 MiB. Requires Service Administrator or an application role with Migrations - Administer. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `oauthCredential` | string | Yes | Reusable Oracle EPM service-account credential for a PCM environment | +| `fileName` | string | Yes | Filename without a folder for upload/import/export; download uses the listed profitoutbox path | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | file | Canonical Sim UserFile stored in this execution | + + diff --git a/apps/sim/blocks/blocks/oracle_epm_profitability.ts b/apps/sim/blocks/blocks/oracle_epm_profitability.ts new file mode 100644 index 00000000000..9bf1b36b68a --- /dev/null +++ b/apps/sim/blocks/blocks/oracle_epm_profitability.ts @@ -0,0 +1,995 @@ +import { NetSuiteIcon } from '@/components/icons' +import { getScopesForService } from '@/lib/oauth/utils' +import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' +import type { OraclePcmResponse } from '@/tools/oracle_epm_profitability/types' +import { parseOraclePcmBoolean } from '@/tools/oracle_epm_profitability/utils' + +export const OraclePcmBlock: BlockConfig = { + type: 'oracle_epm_profitability', + name: 'Oracle EPM Profitability and Cost Management (PCM)', + description: 'Calculate allocations and manage PCM data, POVs, reports, and tasks', + longDescription: + 'Automate Cloud PCM Management Ledger applications using the epm/rest/v1 API family. This is distinct from Enterprise Profitability and Cost Management (EPCM). Use a provisioned PCM environment under an eligible EPM Enterprise or legacy PCM subscription. Most operations require Service Administrator or Power User; creating an application requires Service Administrator. Report and query-export submissions accept all application roles, but task polling requires Service Administrator or Power User. File operations and file selectors require Service Administrator or an application role with Migrations - Administer. Upload inputs to profitinbox and download reports from profitoutbox. Downloads require repository paths of at most 255 UTF-8 bytes; use short output filenames. Submission and waiting are separate; retain processName and inspect the Job Library after ambiguous failures. Application, POV, model view, rule, and query names are manual inputs. Template export and undocumented nested rule-balancing fields are not exposed.', + docsLink: 'https://docs.sim.ai/integrations/oracle_epm_profitability', + category: 'tools', + integrationType: IntegrationType.Analytics, + authMode: AuthMode.ApiKey, + bgColor: '#FFFFFF', + icon: NetSuiteIcon, + canvasPresentation: { + defaultTitle: 'Oracle PCM', + sentences: { + byOperation: { + oracle_epm_profitability_create_application: ['Create application'], + oracle_epm_profitability_enable_application: ['Enable application'], + oracle_epm_profitability_deploy_cube: ['Deploy cube'], + oracle_epm_profitability_update_dimensions: ['Update dimensions'], + oracle_epm_profitability_load_data: ['Load data'], + oracle_epm_profitability_run_calculation: ['Run calculation'], + oracle_epm_profitability_copy_pov: ['Copy POV'], + oracle_epm_profitability_clear_pov: ['Clear POV'], + oracle_epm_profitability_get_rule_balancing: ['Get rule balancing'], + oracle_epm_profitability_generate_program_documentation: ['Generate program documentation'], + oracle_epm_profitability_export_query_results: ['Export query results'], + oracle_epm_profitability_import_template: ['Import template'], + oracle_epm_profitability_apply_data_grants: ['Apply data grants'], + oracle_epm_profitability_merge_slices: ['Merge slices'], + oracle_epm_profitability_optimize_cube: ['Optimize cube'], + oracle_epm_profitability_get_task_status: ['Get task status'], + oracle_epm_profitability_wait_for_task: ['Wait for task'], + oracle_epm_profitability_upload_file: ['Upload file'], + oracle_epm_profitability_list_files: ['List files'], + oracle_epm_profitability_download_file: ['Download file'], + }, + }, + }, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Application', id: 'oracle_epm_profitability_create_application' }, + { label: 'Enable Application', id: 'oracle_epm_profitability_enable_application' }, + { label: 'Deploy Cube', id: 'oracle_epm_profitability_deploy_cube' }, + { label: 'Update Dimensions', id: 'oracle_epm_profitability_update_dimensions' }, + { label: 'Load Data', id: 'oracle_epm_profitability_load_data' }, + { label: 'Run Calculation', id: 'oracle_epm_profitability_run_calculation' }, + { label: 'Copy POV', id: 'oracle_epm_profitability_copy_pov' }, + { label: 'Clear POV', id: 'oracle_epm_profitability_clear_pov' }, + { label: 'Get Rule Balancing', id: 'oracle_epm_profitability_get_rule_balancing' }, + { + label: 'Generate Program Documentation', + id: 'oracle_epm_profitability_generate_program_documentation', + }, + { label: 'Export Query Results', id: 'oracle_epm_profitability_export_query_results' }, + { label: 'Import Template', id: 'oracle_epm_profitability_import_template' }, + { label: 'Apply Data Grants', id: 'oracle_epm_profitability_apply_data_grants' }, + { label: 'Merge Slices', id: 'oracle_epm_profitability_merge_slices' }, + { label: 'Optimize Cube', id: 'oracle_epm_profitability_optimize_cube' }, + { label: 'Get Task Status', id: 'oracle_epm_profitability_get_task_status' }, + { label: 'Wait for Task', id: 'oracle_epm_profitability_wait_for_task' }, + { label: 'Upload File', id: 'oracle_epm_profitability_upload_file' }, + { label: 'List Files', id: 'oracle_epm_profitability_list_files' }, + { label: 'Download File', id: 'oracle_epm_profitability_download_file' }, + ], + value: () => 'oracle_epm_profitability_list_files', + }, + { + id: 'oauthCredential', + title: 'Oracle EPM Account', + type: 'oauth-input', + serviceId: 'oracle-epm-profitability', + credentialKind: 'service-account', + requiredScopes: getScopesForService('oracle-epm-profitability'), + required: true, + placeholder: 'Select an Oracle EPM service account', + }, + { + id: 'applicationName', + title: 'Application name', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'oracle_epm_profitability_create_application', + 'oracle_epm_profitability_enable_application', + 'oracle_epm_profitability_deploy_cube', + 'oracle_epm_profitability_update_dimensions', + 'oracle_epm_profitability_load_data', + 'oracle_epm_profitability_run_calculation', + 'oracle_epm_profitability_copy_pov', + 'oracle_epm_profitability_clear_pov', + 'oracle_epm_profitability_get_rule_balancing', + 'oracle_epm_profitability_generate_program_documentation', + 'oracle_epm_profitability_export_query_results', + 'oracle_epm_profitability_import_template', + 'oracle_epm_profitability_apply_data_grants', + 'oracle_epm_profitability_merge_slices', + 'oracle_epm_profitability_optimize_cube', + ], + }, + required: { + field: 'operation', + value: [ + 'oracle_epm_profitability_create_application', + 'oracle_epm_profitability_enable_application', + 'oracle_epm_profitability_deploy_cube', + 'oracle_epm_profitability_update_dimensions', + 'oracle_epm_profitability_load_data', + 'oracle_epm_profitability_run_calculation', + 'oracle_epm_profitability_copy_pov', + 'oracle_epm_profitability_clear_pov', + 'oracle_epm_profitability_get_rule_balancing', + 'oracle_epm_profitability_generate_program_documentation', + 'oracle_epm_profitability_export_query_results', + 'oracle_epm_profitability_import_template', + 'oracle_epm_profitability_apply_data_grants', + 'oracle_epm_profitability_merge_slices', + 'oracle_epm_profitability_optimize_cube', + ], + }, + placeholder: 'Exact PCM Management Ledger application name', + }, + { + id: 'povName', + title: 'POV name', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'oracle_epm_profitability_run_calculation', + 'oracle_epm_profitability_copy_pov', + 'oracle_epm_profitability_clear_pov', + 'oracle_epm_profitability_get_rule_balancing', + 'oracle_epm_profitability_generate_program_documentation', + ], + }, + required: true, + placeholder: 'POV members joined by stringDelimiter; model POV for calculations', + }, + { + id: 'destinationPovName', + title: 'Destination POV name', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_copy_pov' }, + required: true, + placeholder: 'Destination POV members joined by stringDelimiter', + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + condition: { + field: 'operation', + value: [ + 'oracle_epm_profitability_create_application', + 'oracle_epm_profitability_import_template', + ], + }, + required: true, + placeholder: 'Description of the application', + }, + { + id: 'ruleDimensionName', + title: 'Rule dimension name', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_create_application' }, + required: true, + placeholder: 'Rule dimension name', + }, + { + id: 'balanceDimensionName', + title: 'Balance dimension name', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_create_application' }, + required: true, + placeholder: 'Balance dimension name', + }, + { + id: 'comment', + title: 'Comment', + type: 'long-input', + condition: { + field: 'operation', + value: ['oracle_epm_profitability_deploy_cube', 'oracle_epm_profitability_run_calculation'], + }, + required: { field: 'operation', value: 'oracle_epm_profitability_deploy_cube' }, + placeholder: 'Comment for the operation', + }, + { + id: 'isKeepData', + title: 'Keep existing data', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_deploy_cube' }, + required: true, + defaultValue: false, + }, + { + id: 'isReplaceCube', + title: 'Replace cube', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_deploy_cube' }, + required: true, + defaultValue: false, + }, + { + id: 'dataFileName', + title: 'Data file name', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_update_dimensions' }, + required: true, + placeholder: + 'Filename already staged in profitinbox; dimension updates accept a delimiter-separated list', + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of existing PCM dimension metadata filenames. Return ONLY the filename list.', + placeholder: 'Describe the requested selection', + }, + }, + { + id: 'stringDelimiter', + title: 'String delimiter', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'oracle_epm_profitability_update_dimensions', + 'oracle_epm_profitability_run_calculation', + 'oracle_epm_profitability_copy_pov', + 'oracle_epm_profitability_clear_pov', + 'oracle_epm_profitability_get_rule_balancing', + 'oracle_epm_profitability_generate_program_documentation', + ], + }, + required: { field: 'operation', value: 'oracle_epm_profitability_copy_pov' }, + placeholder: + 'Single separator: defaults to underscore for POVs and comma for dimension file lists', + }, + { + id: 'acceptableDecreasePercentage', + title: 'Acceptable decrease percentage', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_update_dimensions' }, + mode: 'advanced', + placeholder: 'Maximum permitted dimension member decrease percentage, 0 to 100', + }, + { + id: 'clearAllDataFlag', + title: 'Clear existing data', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_load_data' }, + required: true, + defaultValue: false, + }, + { + id: 'dataLoadValue', + title: 'Data load behavior', + type: 'dropdown', + condition: { field: 'operation', value: 'oracle_epm_profitability_load_data' }, + required: true, + options: [ + { label: 'Add to existing values', id: 'ADD_EXISTING_VALUES' }, + { label: 'Overwrite existing values', id: 'OVERWRITE_EXISTING_VALUES' }, + ], + value: () => 'OVERWRITE_EXISTING_VALUES', + placeholder: 'How incoming values affect existing data', + }, + { + id: 'exeType', + title: 'Calculation scope', + type: 'dropdown', + condition: { field: 'operation', value: 'oracle_epm_profitability_run_calculation' }, + required: true, + options: [ + { label: 'All rules', id: 'ALL_RULES' }, + { label: 'Rule-set range', id: 'RULESET_SUBSET' }, + { label: 'Single rule', id: 'SINGLE_RULE' }, + ], + value: () => 'ALL_RULES', + placeholder: 'Rules to execute; dataPOVName requires ALL_RULES', + }, + { + id: 'dataPOVName', + title: 'Data POV name', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_run_calculation' }, + mode: 'advanced', + placeholder: 'Optional different data POV; requires ALL_RULES', + }, + { + id: 'isClearCalculated', + title: 'Clear calculated data', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_run_calculation' }, + mode: 'advanced', + defaultValue: false, + }, + { + id: 'optimizeReporting', + title: 'Optimize reporting', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_run_calculation' }, + mode: 'advanced', + defaultValue: true, + }, + { + id: 'subsetStart', + title: 'Subset start', + type: 'short-input', + condition: (values) => ({ + field: 'operation', + value: [ + 'oracle_epm_profitability_run_calculation', + 'oracle_epm_profitability_generate_program_documentation', + ], + ...(values?.operation === 'oracle_epm_profitability_run_calculation' + ? { and: { field: 'exeType', value: 'RULESET_SUBSET' } } + : {}), + }), + required: { + field: 'operation', + value: 'oracle_epm_profitability_run_calculation', + and: { field: 'exeType', value: 'RULESET_SUBSET' }, + }, + placeholder: 'First rule-set sequence; required for RULESET_SUBSET', + }, + { + id: 'subsetEnd', + title: 'Subset end', + type: 'short-input', + condition: (values) => ({ + field: 'operation', + value: [ + 'oracle_epm_profitability_run_calculation', + 'oracle_epm_profitability_generate_program_documentation', + ], + ...(values?.operation === 'oracle_epm_profitability_run_calculation' + ? { and: { field: 'exeType', value: 'RULESET_SUBSET' } } + : {}), + }), + required: { + field: 'operation', + value: 'oracle_epm_profitability_run_calculation', + and: { field: 'exeType', value: 'RULESET_SUBSET' }, + }, + placeholder: 'Last rule-set sequence; required for RULESET_SUBSET', + }, + { + id: 'ruleName', + title: 'Rule name', + type: 'short-input', + condition: { + field: 'operation', + value: 'oracle_epm_profitability_run_calculation', + and: { field: 'exeType', value: 'SINGLE_RULE' }, + }, + required: true, + placeholder: 'Existing rule name; required for SINGLE_RULE', + }, + { + id: 'ruleSetName', + title: 'Rule set name', + type: 'short-input', + condition: { + field: 'operation', + value: 'oracle_epm_profitability_run_calculation', + and: { field: 'exeType', value: 'SINGLE_RULE' }, + }, + required: true, + placeholder: 'Existing rule set name; required for SINGLE_RULE', + }, + { + id: 'isManageRule', + title: 'Include program rules', + type: 'switch', + condition: { + field: 'operation', + value: ['oracle_epm_profitability_copy_pov', 'oracle_epm_profitability_clear_pov'], + }, + required: { field: 'operation', value: 'oracle_epm_profitability_copy_pov' }, + defaultValue: false, + }, + { + id: 'isInputData', + title: 'Include input data', + type: 'switch', + condition: { + field: 'operation', + value: ['oracle_epm_profitability_copy_pov', 'oracle_epm_profitability_clear_pov'], + }, + required: { field: 'operation', value: 'oracle_epm_profitability_copy_pov' }, + defaultValue: false, + }, + { + id: 'createDestPOV', + title: 'Create destination POV', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_copy_pov' }, + required: true, + defaultValue: false, + }, + { + id: 'modelViewName', + title: 'Model view name', + type: 'short-input', + condition: { + field: 'operation', + value: ['oracle_epm_profitability_copy_pov', 'oracle_epm_profitability_get_rule_balancing'], + }, + required: { field: 'operation', value: 'oracle_epm_profitability_get_rule_balancing' }, + placeholder: 'Existing model view to limit the data slice', + }, + { + id: 'nonEmptyTupleEnabled', + title: 'Optimize nonempty tuples', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_copy_pov' }, + mode: 'advanced', + defaultValue: true, + }, + { + id: 'isAllocatedValues', + title: 'Clear allocated values', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_clear_pov' }, + mode: 'advanced', + defaultValue: false, + }, + { + id: 'isAdjustmentValues', + title: 'Clear adjustment values', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_clear_pov' }, + mode: 'advanced', + defaultValue: false, + }, + { + id: 'queryName', + title: 'Query name', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'oracle_epm_profitability_clear_pov', + 'oracle_epm_profitability_export_query_results', + ], + }, + mode: 'advanced', + placeholder: 'Existing PCM query; exports with no query export all application data', + }, + { + id: 'fileName', + title: 'File name', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'oracle_epm_profitability_upload_file', + 'oracle_epm_profitability_export_query_results', + 'oracle_epm_profitability_generate_program_documentation', + ], + }, + required: { + field: 'operation', + value: [ + 'oracle_epm_profitability_upload_file', + 'oracle_epm_profitability_export_query_results', + ], + }, + placeholder: 'Filename without a repository folder', + }, + { + id: 'fileType', + title: 'File type', + type: 'dropdown', + condition: { + field: 'operation', + value: 'oracle_epm_profitability_generate_program_documentation', + }, + mode: 'advanced', + options: [ + { label: 'PDF', id: 'PDF' }, + { label: 'XML', id: 'XML' }, + { label: 'Word', id: 'WORD' }, + { label: 'Excel', id: 'EXCEL' }, + { label: 'HTML', id: 'HTML' }, + ], + value: () => 'PDF', + placeholder: 'Program documentation format', + }, + { + id: 'skipFilters', + title: 'Skip filters', + type: 'switch', + condition: { + field: 'operation', + value: 'oracle_epm_profitability_generate_program_documentation', + }, + mode: 'advanced', + defaultValue: false, + }, + { + id: 'useAlias', + title: 'Use aliases', + type: 'switch', + condition: { + field: 'operation', + value: 'oracle_epm_profitability_generate_program_documentation', + }, + mode: 'advanced', + defaultValue: false, + }, + { + id: 'exportOnlyLevel0Flg', + title: 'Export only level-0 data', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_export_query_results' }, + mode: 'advanced', + defaultValue: false, + }, + { + id: 'fileOutputOptions', + title: 'File output options', + type: 'dropdown', + condition: { field: 'operation', value: 'oracle_epm_profitability_export_query_results' }, + mode: 'advanced', + options: [ + { label: 'ZIP only', id: 'ZIP_ONLY' }, + { label: 'ZIP and text', id: 'ZIP_AND_TEXT' }, + { label: 'Text only', id: 'TEXT_ONLY' }, + ], + value: () => 'ZIP_ONLY', + placeholder: 'Export compression and text-file output', + }, + { + id: 'roundingPrecision', + title: 'Rounding precision', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_export_query_results' }, + mode: 'advanced', + placeholder: 'Decimal places for named query exports; Oracle defaults to 2', + }, + { + id: 'dataFormat', + title: 'Data format', + type: 'dropdown', + condition: { field: 'operation', value: 'oracle_epm_profitability_export_query_results' }, + mode: 'advanced', + options: [ + { label: 'Native', id: 'NATIVE' }, + { label: 'Columnar', id: 'COLUMNAR' }, + ], + value: () => 'NATIVE', + placeholder: + 'NATIVE exports query data; COLUMNAR exports application data and ignores queryName', + }, + { + id: 'memberFilters', + title: 'Member filters', + type: 'long-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_export_query_results' }, + mode: 'advanced', + placeholder: + 'JSON string mapping dimensions to level-0 member arrays; applies to COLUMNAR exports', + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object mapping dimension names to arrays of level-0 member names. Return ONLY the JSON object.', + placeholder: 'Describe the requested selection', + generationType: 'json-object', + }, + }, + { + id: 'includeHeader', + title: 'Include header', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_export_query_results' }, + mode: 'advanced', + defaultValue: true, + }, + { + id: 'delimiter', + title: 'Delimiter', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_export_query_results' }, + mode: 'advanced', + placeholder: 'Single column separator for COLUMNAR exports; Oracle defaults to space', + }, + { + id: 'keepDuplicateMemberFormat', + title: 'Keep duplicate member format', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_export_query_results' }, + mode: 'advanced', + defaultValue: true, + }, + { + id: 'isApplicationOverwrite', + title: 'Overwrite application', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_import_template' }, + required: true, + defaultValue: false, + }, + { + id: 'removeZeroCells', + title: 'Remove zero cells', + type: 'switch', + condition: { field: 'operation', value: 'oracle_epm_profitability_merge_slices' }, + mode: 'advanced', + defaultValue: false, + }, + { + id: 'type', + title: 'Optimization operation', + type: 'dropdown', + condition: { field: 'operation', value: 'oracle_epm_profitability_optimize_cube' }, + required: true, + options: [ + { label: 'Clear aggregations', id: 'clearAggregations' }, + { label: 'Create aggregations', id: 'createAggregations' }, + { label: 'Start query tracking', id: 'startQueryTracking' }, + { label: 'Stop query tracking', id: 'stopQueryTracking' }, + { label: 'Create query-based aggregations', id: 'createQBOAggregations' }, + ], + placeholder: 'ASO optimization operation', + }, + { + id: 'processName', + title: 'Process name', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'oracle_epm_profitability_get_task_status', + 'oracle_epm_profitability_wait_for_task', + ], + }, + required: true, + placeholder: 'Task processName returned by PCM submission; not a numeric EPCM job ID', + }, + { + id: 'maxWaitSeconds', + title: 'Max wait seconds', + type: 'short-input', + condition: { field: 'operation', value: 'oracle_epm_profitability_wait_for_task' }, + mode: 'advanced', + value: () => '300', + placeholder: 'Maximum wait in seconds, 1 to 3600; does not cancel the remote task', + }, + { + id: 'inputFileSelector', + title: 'Input file', + type: 'file-selector', + canonicalParamId: 'inputFileName', + mode: 'basic', + required: true, + condition: { + field: 'operation', + value: ['oracle_epm_profitability_load_data', 'oracle_epm_profitability_import_template'], + }, + selectorKey: 'oracleEpmPcm.inputFiles', + serviceId: 'oracle-epm-profitability', + dependsOn: ['oauthCredential'], + }, + { + id: 'inputFileManual', + title: 'Input file', + type: 'short-input', + canonicalParamId: 'inputFileName', + mode: 'advanced', + required: true, + condition: { + field: 'operation', + value: ['oracle_epm_profitability_load_data', 'oracle_epm_profitability_import_template'], + }, + placeholder: 'Filename in profitinbox without its folder', + }, + { + id: 'outputFileSelector', + title: 'Output file', + type: 'file-selector', + canonicalParamId: 'downloadFileName', + mode: 'basic', + required: true, + condition: { field: 'operation', value: 'oracle_epm_profitability_download_file' }, + selectorKey: 'oracleEpmPcm.outputFiles', + serviceId: 'oracle-epm-profitability', + dependsOn: ['oauthCredential'], + }, + { + id: 'outputFileManual', + title: 'Output file', + type: 'short-input', + canonicalParamId: 'downloadFileName', + mode: 'advanced', + required: true, + condition: { field: 'operation', value: 'oracle_epm_profitability_download_file' }, + placeholder: 'Exact listed profitoutbox path', + }, + { + id: 'uploadFile', + title: 'File', + type: 'file-upload', + canonicalParamId: 'file', + mode: 'basic', + required: true, + multiple: false, + maxSize: 100, + condition: { field: 'operation', value: 'oracle_epm_profitability_upload_file' }, + }, + { + id: 'fileRef', + title: 'File', + type: 'short-input', + canonicalParamId: 'file', + mode: 'advanced', + required: true, + placeholder: 'Reference a Sim UserFile', + condition: { field: 'operation', value: 'oracle_epm_profitability_upload_file' }, + }, + ], + tools: { + access: [ + 'oracle_epm_profitability_create_application', + 'oracle_epm_profitability_enable_application', + 'oracle_epm_profitability_deploy_cube', + 'oracle_epm_profitability_update_dimensions', + 'oracle_epm_profitability_load_data', + 'oracle_epm_profitability_run_calculation', + 'oracle_epm_profitability_copy_pov', + 'oracle_epm_profitability_clear_pov', + 'oracle_epm_profitability_get_rule_balancing', + 'oracle_epm_profitability_generate_program_documentation', + 'oracle_epm_profitability_export_query_results', + 'oracle_epm_profitability_import_template', + 'oracle_epm_profitability_apply_data_grants', + 'oracle_epm_profitability_merge_slices', + 'oracle_epm_profitability_optimize_cube', + 'oracle_epm_profitability_get_task_status', + 'oracle_epm_profitability_wait_for_task', + 'oracle_epm_profitability_upload_file', + 'oracle_epm_profitability_list_files', + 'oracle_epm_profitability_download_file', + ], + config: { + tool: (params) => params.operation, + params: (params) => { + const result: Record = {} + for (const key of [ + 'acceptableDecreasePercentage', + 'subsetStart', + 'subsetEnd', + 'roundingPrecision', + 'maxWaitSeconds', + ]) { + if (params[key] !== undefined && params[key] !== null && params[key] !== '') { + result[key] = Number(params[key]) + } + } + for (const key of [ + 'isKeepData', + 'isReplaceCube', + 'clearAllDataFlag', + 'isClearCalculated', + 'optimizeReporting', + 'isManageRule', + 'isInputData', + 'createDestPOV', + 'nonEmptyTupleEnabled', + 'isAllocatedValues', + 'isAdjustmentValues', + 'skipFilters', + 'useAlias', + 'exportOnlyLevel0Flg', + 'includeHeader', + 'keepDuplicateMemberFormat', + 'isApplicationOverwrite', + 'removeZeroCells', + ]) { + result[key] = parseOraclePcmBoolean(params[key]) + } + if (params.operation === 'oracle_epm_profitability_load_data') { + result.dataFileName = params.inputFileName + } + if (params.operation === 'oracle_epm_profitability_import_template') { + result.fileName = params.inputFileName + } + if (params.operation === 'oracle_epm_profitability_download_file') { + result.fileName = params.downloadFileName + } + if (params.operation === 'oracle_epm_profitability_upload_file') { + result.file = normalizeFileInput(params.file, { single: true }) + } + return result + }, + }, + }, + inputs: { + oauthCredential: { type: 'string', description: 'Reusable Oracle EPM credential' }, + inputFileName: { type: 'string', description: 'Filename in profitinbox' }, + downloadFileName: { type: 'string', description: 'Listed PCM repository path' }, + applicationName: { type: 'string', description: 'PCM Management Ledger application name' }, + povName: { type: 'string', description: 'POV members joined by stringDelimiter' }, + destinationPovName: { type: 'string', description: 'Destination POV members' }, + description: { type: 'string', description: 'Application description' }, + ruleDimensionName: { type: 'string', description: 'Rule dimension name' }, + balanceDimensionName: { type: 'string', description: 'Balance dimension name' }, + comment: { type: 'string', description: 'Operation comment' }, + isKeepData: { type: 'boolean', description: 'Preserve existing cube data' }, + isReplaceCube: { type: 'boolean', description: 'Replace the existing cube' }, + dataFileName: { type: 'string', description: 'Staged dimension filenames' }, + stringDelimiter: { type: 'string', description: 'POV or dimension filename separator' }, + acceptableDecreasePercentage: { + type: 'number', + description: 'Allowed member decrease percent', + }, + clearAllDataFlag: { type: 'boolean', description: 'Clear existing data before loading' }, + dataLoadValue: { type: 'string', description: 'Add or overwrite incoming values' }, + exeType: { type: 'string', description: 'All rules, a rule-set range, or one rule' }, + dataPOVName: { type: 'string', description: 'Optional different data POV for ALL_RULES' }, + isClearCalculated: { type: 'boolean', description: 'Clear previously calculated data' }, + optimizeReporting: { type: 'boolean', description: 'Aggregate after calculation' }, + subsetStart: { type: 'number', description: 'First rule-set sequence' }, + subsetEnd: { type: 'number', description: 'Last rule-set sequence' }, + ruleName: { type: 'string', description: 'Existing rule name for SINGLE_RULE' }, + ruleSetName: { type: 'string', description: 'Existing rule set for SINGLE_RULE' }, + isManageRule: { type: 'boolean', description: 'Include program rules' }, + isInputData: { type: 'boolean', description: 'Include input data' }, + createDestPOV: { type: 'boolean', description: 'Create the destination POV if missing' }, + modelViewName: { type: 'string', description: 'Existing model view' }, + nonEmptyTupleEnabled: { type: 'boolean', description: 'Optimize nonempty tuples' }, + isAllocatedValues: { type: 'boolean', description: 'Clear allocated values' }, + isAdjustmentValues: { type: 'boolean', description: 'Clear adjustment values' }, + queryName: { type: 'string', description: 'Existing PCM query' }, + fileName: { type: 'string', description: 'Filename for upload or report output' }, + fileType: { type: 'string', description: 'Program documentation format' }, + skipFilters: { type: 'boolean', description: 'Use member counts instead of rule filters' }, + useAlias: { type: 'boolean', description: 'Use aliases in program documentation' }, + exportOnlyLevel0Flg: { type: 'boolean', description: 'Export only level-0 query data' }, + fileOutputOptions: { type: 'string', description: 'ZIP and text output options' }, + roundingPrecision: { type: 'number', description: 'Query export decimal places' }, + dataFormat: { type: 'string', description: 'Native query or columnar application export' }, + memberFilters: { type: 'string', description: 'JSON string of dimension member filters' }, + includeHeader: { type: 'boolean', description: 'Include columnar dimension headers' }, + delimiter: { type: 'string', description: 'Single columnar separator' }, + keepDuplicateMemberFormat: { type: 'boolean', description: 'Qualify duplicate members' }, + isApplicationOverwrite: { type: 'boolean', description: 'Overwrite the imported application' }, + removeZeroCells: { type: 'boolean', description: 'Remove zero-valued cells when merging' }, + type: { type: 'string', description: 'ASO optimization operation' }, + processName: { type: 'string', description: 'PCM task identifier returned by submission' }, + maxWaitSeconds: { type: 'number', description: 'Maximum wait in seconds, 1 to 3600' }, + file: { type: 'json', description: 'One canonical Sim UserFile or one-element upload array' }, + }, + outputs: { + processName: { + type: 'string', + description: 'PCM task identifier for status and wait operations', + }, + status: { type: 'number', description: '-1 pending, 0 succeeded, positive values failed' }, + state: { type: 'string', description: 'Normalized pending, succeeded, or failed state' }, + statusMessage: { type: 'string', description: 'Oracle status message' }, + details: { type: 'string', description: 'Oracle task details, including report information' }, + attempts: { type: 'number', description: 'Status reads before a terminal wait result' }, + timedOut: { type: 'boolean', description: 'Wait expired without cancelling the remote task' }, + items: { + type: 'json', + description: + 'Scalar rule balances: ruleNumber, name, description, sequence, scale, balanceTypeRule, balances, allocations, adjustments, input, remainders, netChange, and offset', + }, + files: { type: 'json', description: 'PCM files with name, type, size, and lastModifiedTime' }, + fileName: { type: 'string', description: 'Uploaded repository path in profitinbox' }, + file: { type: 'file', description: 'Downloaded canonical Sim UserFile' }, + }, +} + +export const OraclePcmBlockMeta = { + tags: ['data-analytics', 'automation'], + url: 'https://www.oracle.com/performance-management/profitability-cost-management/', + templates: [ + { + icon: NetSuiteIcon, + title: 'Refresh input data', + prompt: + 'When a PCM data file is ready, upload it to profitinbox, load it into the selected application, and wait for the returned task.', + modules: ['workflows', 'files'], + category: 'operations', + tags: ['automation'], + }, + { + icon: NetSuiteIcon, + title: 'Run monthly allocations', + prompt: + 'On a monthly schedule, run all rules for the selected PCM POV and wait for completion before reporting the task outcome.', + modules: ['workflows', 'files'], + category: 'operations', + tags: ['automation'], + }, + { + icon: NetSuiteIcon, + title: 'Compare rule balances', + prompt: + 'After a PCM calculation, retrieve rule-balancing results for a chosen POV and model view, then summarize the documented balances and remainders.', + modules: ['workflows', 'files'], + category: 'operations', + tags: ['automation'], + }, + { + icon: NetSuiteIcon, + title: 'Document allocation rules', + prompt: + 'When allocation documentation is needed, submit a program report for the chosen POV, wait for completion, then list and download the report from profitoutbox.', + modules: ['workflows', 'files'], + category: 'operations', + tags: ['automation'], + }, + { + icon: NetSuiteIcon, + title: 'Export profitability results', + prompt: + 'On a reporting schedule, export an existing PCM query, wait for its task, then list and download the output file.', + modules: ['workflows', 'files'], + category: 'operations', + tags: ['automation'], + }, + { + icon: NetSuiteIcon, + title: 'Update dimension metadata', + prompt: + 'When dimension metadata files are ready, stage them in profitinbox, update dimensions with a member-decrease safeguard, then wait for the task result.', + modules: ['workflows', 'files'], + category: 'operations', + tags: ['automation'], + }, + { + icon: NetSuiteIcon, + title: 'Prepare a scenario POV', + prompt: + 'When a new scenario is requested, copy selected rules and input data to its PCM POV and wait for the copy task.', + modules: ['workflows', 'files'], + category: 'operations', + tags: ['automation'], + }, + ], + skills: [ + { + name: 'load-pcm-inputs', + description: 'Load PCM inputs using documented PCM operations.', + content: + '# Load PCM Inputs\n\n## Steps\nUpload the supplied data file to profitinbox. Load the file into the selected PCM application. Wait using the returned processName.\n\n## Output\nReturn the task identifiers and results. Stop on failures and retain processName for follow-up.', + }, + { + name: 'calculate-pcm-allocations', + description: 'Calculate PCM allocations using documented PCM operations.', + content: + '# Calculate PCM Allocations\n\n## Steps\nUse the supplied application and POV names. Select all rules, a rule-set range, or one rule. Submit the calculation and wait for its processName.\n\n## Output\nReturn the task identifiers and results. Stop on failures and retain processName for follow-up.', + }, + { + name: 'export-pcm-query', + description: 'Export PCM query results using documented PCM operations.', + content: + '# Export PCM Query Results\n\n## Steps\nUse an existing named PCM query and choose an output filename. Submit export query results. Wait for the processName, then list and download the output file.\n\n## Output\nReturn the task identifiers and downloaded files. Stop on failures and retain processName for follow-up.', + }, + { + name: 'inspect-pcm-rule-balancing', + description: 'Inspect PCM rule balancing using documented PCM operations.', + content: + '# Inspect PCM Rule Balancing\n\n## Steps\nRead rule balancing for the supplied application, POV, and model view. Compare balances, allocations, adjustments, and remainders. Do not infer omitted nested rule details.\n\n## Output\nReturn the documented scalar rule balances or report the failure.', + }, + { + name: 'document-pcm-program', + description: 'Document PCM programs using documented PCM operations.', + content: + '# Document PCM Program\n\n## Steps\nSubmit program documentation for the supplied application and POV. Choose a format and filename. Wait for the processName, then list and download the report.\n\n## Output\nReturn the task identifiers and downloaded files. Stop on failures and retain processName for follow-up.', + }, + { + name: 'optimize-pcm-aso', + description: 'Optimize PCM ASO queries using documented PCM operations.', + content: + '# Optimize PCM ASO Queries\n\n## Steps\nUse the selected PCM application. Start query tracking, run representative existing query exports, create query-based aggregates, and stop tracking. Wait for each task before the next operation.\n\n## Output\nReturn the task identifiers and results. Stop on failures and retain processName for follow-up.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry-maps.ts b/apps/sim/blocks/registry-maps.ts index 3b161aa97e4..602b7c1cf59 100644 --- a/apps/sim/blocks/registry-maps.ts +++ b/apps/sim/blocks/registry-maps.ts @@ -252,6 +252,7 @@ import { OktaBlock, OktaBlockMeta } from '@/blocks/blocks/okta' import { OneDriveBlock, OneDriveBlockMeta } from '@/blocks/blocks/onedrive' import { OnePasswordBlock, OnePasswordBlockMeta } from '@/blocks/blocks/onepassword' import { OpenAIBlock, OpenAIBlockMeta } from '@/blocks/blocks/openai' +import { OraclePcmBlock, OraclePcmBlockMeta } from '@/blocks/blocks/oracle_epm_profitability' import { OutlookBlock, OutlookBlockMeta } from '@/blocks/blocks/outlook' import { PagerDutyBlock, PagerDutyBlockMeta } from '@/blocks/blocks/pagerduty' import { ParallelBlock, ParallelBlockMeta } from '@/blocks/blocks/parallel' @@ -595,6 +596,7 @@ export const BLOCK_REGISTRY: Record = { onedrive: OneDriveBlock, onepassword: OnePasswordBlock, openai: OpenAIBlock, + oracle_epm_profitability: OraclePcmBlock, outlook: OutlookBlock, pagerduty: PagerDutyBlock, parallel_ai: ParallelBlock, @@ -917,6 +919,7 @@ export const BLOCK_META_REGISTRY: Record = { onedrive: OneDriveBlockMeta, onepassword: OnePasswordBlockMeta, openai: OpenAIBlockMeta, + oracle_epm_profitability: OraclePcmBlockMeta, outlook: OutlookBlockMeta, pagerduty: PagerDutyBlockMeta, parallel_ai: ParallelBlockMeta, diff --git a/apps/sim/lib/copilot/generated/docs-manifest.ts b/apps/sim/lib/copilot/generated/docs-manifest.ts index 1b9e432d050..b84c507d960 100644 --- a/apps/sim/lib/copilot/generated/docs-manifest.ts +++ b/apps/sim/lib/copilot/generated/docs-manifest.ts @@ -245,6 +245,7 @@ export const DOCS_MANIFEST: readonly string[] = [ 'integrations/onedrive.mdx', 'integrations/onepassword.mdx', 'integrations/openai.mdx', + 'integrations/oracle_epm_profitability.mdx', 'integrations/outlook.mdx', 'integrations/pagerduty.mdx', 'integrations/parallel_ai.mdx', diff --git a/apps/sim/lib/integrations/credential-display.test.ts b/apps/sim/lib/integrations/credential-display.test.ts index f73da60137f..e1572163834 100644 --- a/apps/sim/lib/integrations/credential-display.test.ts +++ b/apps/sim/lib/integrations/credential-display.test.ts @@ -65,6 +65,7 @@ const EXPECTED_COVERAGE: Record = { 'linear-service-account': ['linear'], 'monday-service-account': ['monday'], 'notion-service-account': ['notion'], + 'oracle-epm-service-account': [], // NetSuite remains an API-key catalog integration, like Snowflake, while its // block uses the shared reusable-credential selector. 'netsuite-service-account': [], @@ -110,6 +111,28 @@ describe('service-account coverage', () => { }) }) + it('binds PCM to the existing reusable Oracle EPM credential', () => { + expect( + OAUTH_PROVIDERS['oracle-epm-profitability'].services['oracle-epm-profitability'] + ).toMatchObject({ + providerId: 'oracle-epm-profitability', + serviceAccountProviderId: 'oracle-epm-service-account', + authType: 'service_account', + }) + expect( + credentialProviderMatchesService( + 'oracle-epm-service-account', + OAUTH_PROVIDERS['oracle-epm-profitability'].services['oracle-epm-profitability'] + ) + ).toBe(true) + expect( + credentialProviderMatchesService( + 'netsuite-service-account', + OAUTH_PROVIDERS['oracle-epm-profitability'].services['oracle-epm-profitability'] + ) + ).toBe(false) + }) + it('pins the table to exactly the registered service-account provider ids', () => { expect(REGISTERED_SERVICE_ACCOUNT_IDS).toEqual(Object.keys(EXPECTED_COVERAGE).sort()) }) diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index 34b99c0e128..5caa383ac63 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -464,6 +464,7 @@ export const blockTypeToIconMap: Record = { okta: OktaIcon, onedrive: MicrosoftOneDriveIcon, onepassword: OnePasswordIcon, + oracle_epm_profitability: NetSuiteIcon, outlook: OutlookIcon, pagerduty: PagerDutyIcon, parallel_ai: ParallelIcon, diff --git a/apps/sim/lib/internal/oracle-epm-profitability/execute-tool.test.ts b/apps/sim/lib/internal/oracle-epm-profitability/execute-tool.test.ts new file mode 100644 index 00000000000..c700310be61 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/execute-tool.test.ts @@ -0,0 +1,125 @@ +/** @vitest-environment node */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ operation: vi.fn(), job: vi.fn(), file: vi.fn() })) +vi.mock('@/lib/internal/oracle-epm-profitability/operations', () => ({ + executeOraclePcmOperation: mocks.operation, +})) +vi.mock('@/lib/internal/oracle-epm-profitability/jobs', () => ({ + executeOraclePcmJobOperation: mocks.job, +})) +vi.mock('@/lib/internal/oracle-epm-profitability/files.server', () => ({ + executeOraclePcmFileOperation: mocks.file, +})) +vi.mock('@/lib/internal/oracle-epm/files.server', () => ({ + openOracleEpmSourceFile: vi.fn(), + storeOracleEpmDownload: vi.fn(), +})) + +import { oracleEpmLocalError } from '@/lib/internal/oracle-epm/errors' +import { executeOraclePcmTool } from '@/lib/internal/oracle-epm-profitability/execute-tool' +import { OraclePcmOperationError } from '@/lib/internal/oracle-epm-profitability/normalizers' +import type { InternalToolOperationCall } from '@/lib/internal/tool-operations/types' + +function call(operation: string): InternalToolOperationCall { + return { + toolId: `oracle_epm_profitability_${operation}`, + input: { + oauthCredential: 'credential-1', + accessToken: 'injected-token', + instanceUrl: 'https://epm.example.com', + userId: 'untrusted', + }, + context: { workflowId: 'workflow-1', userId: 'trusted-user' }, + headers: new Headers(), + requestId: 'request-1', + signal: new AbortController().signal, + } +} +describe('Oracle PCM direct tool handler', () => { + beforeEach(() => { + vi.clearAllMocks() + for (const mock of Object.values(mocks)) mock.mockResolvedValue({ success: true, output: {} }) + }) + it.each([ + 'create_application', + 'enable_application', + 'deploy_cube', + 'update_dimensions', + 'load_data', + 'run_calculation', + 'copy_pov', + 'clear_pov', + 'get_rule_balancing', + 'generate_program_documentation', + 'export_query_results', + 'import_template', + 'apply_data_grants', + 'merge_slices', + 'optimize_cube', + ])('dispatches %s without an internal HTTP hop', async (operation) => { + const request = call(operation) + expect((await executeOraclePcmTool(request)).status).toBe(200) + expect(mocks.operation).toHaveBeenCalledWith(operation, request.input, request.signal) + }) + it.each(['get_task_status', 'wait_for_task'])( + 'forwards cancellation for %s', + async (operation) => { + const request = call(operation) + await executeOraclePcmTool(request) + expect(mocks.job).toHaveBeenCalledWith(operation, request.input, request.signal) + } + ) + it.each(['list_files', 'upload_file', 'download_file'])( + 'forwards trusted context for %s', + async (operation) => { + const request = call(operation) + await executeOraclePcmTool(request) + expect(mocks.file).toHaveBeenCalledWith( + operation, + request.input, + request.signal, + request.context + ) + } + ) + it.each([ + [new OraclePcmOperationError('Invalid application', 400), 400, 'Invalid application'], + [oracleEpmLocalError('invalid_response'), 502, 'Oracle EPM returned an invalid response'], + [oracleEpmLocalError('timeout'), 408, 'The Oracle EPM request timed out'], + [ + oracleEpmLocalError('payload_too_large'), + 413, + 'The Oracle EPM payload exceeded the allowed size', + ], + [new Error('credential-secret-canary'), 500, 'Oracle PCM operation failed'], + ])( + 'returns safe failures and disallows automatic resubmission', + async (error, status, message) => { + mocks.operation.mockRejectedValue(error) + const response = await executeOraclePcmTool(call('run_calculation')) + expect(response.status).toBe(status) + expect(await response.json()).toEqual({ + success: false, + output: {}, + error: message, + retryable: false, + }) + } + ) + it('distinguishes local timeout from remote cancellation', async () => { + mocks.job.mockRejectedValue(new DOMException('deadline', 'TimeoutError')) + const response = await executeOraclePcmTool(call('wait_for_task')) + expect(response.status).toBe(408) + expect((await response.json()).error).toContain('remote task was not cancelled') + }) + it('propagates aborts and rejects other tool namespaces', async () => { + const request = call('run_calculation') + request.signal = AbortSignal.abort() + await expect(executeOraclePcmTool(request)).rejects.toThrow() + expect(mocks.operation).not.toHaveBeenCalled() + expect( + (await executeOraclePcmTool({ ...call('list_files'), toolId: 'unrelated_tool' })).status + ).toBe(400) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm-profitability/execute-tool.ts b/apps/sim/lib/internal/oracle-epm-profitability/execute-tool.ts new file mode 100644 index 00000000000..9b84e292183 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/execute-tool.ts @@ -0,0 +1,64 @@ +import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { OracleEpmError } from '@/lib/internal/oracle-epm' +import { executeOraclePcmFileOperation } from '@/lib/internal/oracle-epm-profitability/files.server' +import { executeOraclePcmJobOperation } from '@/lib/internal/oracle-epm-profitability/jobs' +import { OraclePcmOperationError } from '@/lib/internal/oracle-epm-profitability/normalizers' +import { executeOraclePcmOperation } from '@/lib/internal/oracle-epm-profitability/operations' +import type { InternalToolOperationHandler } from '@/lib/internal/tool-operations/types' + +const PREFIX = 'oracle_epm_profitability_' +const JOB_OPERATIONS = new Set(['get_task_status', 'wait_for_task']) +const FILE_OPERATIONS = new Set(['list_files', 'upload_file', 'download_file']) + +export const executeOraclePcmTool: InternalToolOperationHandler = async (request) => { + request.signal?.throwIfAborted() + if (!request.toolId.startsWith(PREFIX)) { + return Response.json( + { success: false, output: {}, error: 'Unsupported Oracle PCM tool', retryable: false }, + { status: 400 } + ) + } + const operation = request.toolId.slice(PREFIX.length) + try { + const result = FILE_OPERATIONS.has(operation) + ? await executeOraclePcmFileOperation( + operation, + request.input, + request.signal, + request.context + ) + : JOB_OPERATIONS.has(operation) + ? await executeOraclePcmJobOperation(operation, request.input, request.signal) + : await executeOraclePcmOperation(operation, request.input, request.signal) + return Response.json(result) + } catch (error) { + request.signal?.throwIfAborted() + const timeout = error instanceof Error && error.name === 'TimeoutError' + const status = isPayloadSizeLimitError(error) + ? 413 + : error instanceof OraclePcmOperationError + ? error.status + : error instanceof OracleEpmError + ? (error.status ?? + (error.category === 'timeout' + ? 408 + : error.category === 'payload_too_large' + ? 413 + : 502)) + : timeout + ? 408 + : 500 + const message = + error instanceof OraclePcmOperationError || error instanceof OracleEpmError + ? error.message + : timeout + ? 'Oracle PCM wait timed out; the remote task was not cancelled. Resume with the same processName.' + : isPayloadSizeLimitError(error) + ? 'Oracle PCM payload exceeded its allowed size' + : 'Oracle PCM operation failed' + return Response.json( + { success: false, output: {}, error: message, retryable: false }, + { status } + ) + } +} diff --git a/apps/sim/lib/internal/oracle-epm-profitability/files.server.test.ts b/apps/sim/lib/internal/oracle-epm-profitability/files.server.test.ts new file mode 100644 index 00000000000..ac516105df7 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/files.server.test.ts @@ -0,0 +1,300 @@ +/** @vitest-environment node */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ fetch: vi.fn(), dns: vi.fn(), open: vi.fn(), store: vi.fn() })) +vi.mock('@/lib/core/security/input-validation.server', () => ({ + DEFAULT_MAX_RESPONSE_BYTES: 100 * 1024 * 1024, + secureFetchWithPinnedIP: mocks.fetch, + validateUrlWithDNS: mocks.dns, +})) +vi.mock('@/lib/internal/oracle-epm/files.server', () => ({ + openOracleEpmSourceFile: mocks.open, + storeOracleEpmDownload: mocks.store, +})) + +import { + executeOraclePcmFileOperation, + PCM_MAX_TRANSFER_BYTES, +} from '@/lib/internal/oracle-epm-profitability/files.server' + +const auth = { + oauthCredential: 'credential-1', + accessToken: Buffer.from('test-user:test-password').toString('base64'), + instanceUrl: 'https://epm.example.com/gateway', +} +const context = { + userId: 'user-1', + workspaceId: '00000000-0000-4000-8000-000000000001', + workflowId: '00000000-0000-4000-8000-000000000002', + executionId: '00000000-0000-4000-8000-000000000003', +} +const file = { + id: 'file-1', + key: 'execution/report.csv', + name: 'report.csv', + size: 3, + type: 'text/csv', + url: 'https://storage.example/report.csv', +} +const json = (value: unknown) => + new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } }) +const listing = (name = 'profitoutbox/report final.csv', size = '3', type = 'EXTERNAL') => + json({ + status: 0, + items: [{ name, size, type, lastmodifiedtime: '1422534438000' }], + }) + +describe('Oracle PCM ordinary repository files', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.dns.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' }) + mocks.fetch.mockImplementation(() => json({ status: 0 })) + mocks.open.mockResolvedValue({ + fileName: 'report.csv', + contentType: 'text/csv', + maxBytes: PCM_MAX_TRANSFER_BYTES, + chunks: (async function* () { + yield Buffer.from('abc') + })(), + }) + mocks.store.mockImplementation(async ({ body }: { body: ReadableStream }) => { + await new Response(body).arrayBuffer() + return file + }) + }) + + it('uploads one canonical file using trusted actor authority and a bounded binary POST', async () => { + const signal = new AbortController().signal + const input = { + ...auth, + fileName: 'report final.csv', + file: [file], + userId: 'untrusted-user', + } + expect( + await executeOraclePcmFileOperation('upload_file', input, signal, context) + ).toMatchObject({ + success: true, + retryable: false, + output: { fileName: `profitinbox/${input.fileName}`, status: 0 }, + }) + expect(mocks.open).toHaveBeenCalledWith({ + file, + userId: context.userId, + maxBytes: PCM_MAX_TRANSFER_BYTES, + signal, + }) + expect(mocks.fetch.mock.calls[0][0]).toBe( + 'https://epm.example.com/gateway/interop/rest/11.1.2.3.600/applicationsnapshots/report%20final.csv/contents?extDirPath=profitinbox' + ) + expect(mocks.fetch.mock.calls[0][2]).toMatchObject({ method: 'POST', body: Buffer.from('abc') }) + expect(mocks.fetch).toHaveBeenCalledTimes(1) + }) + it('rejects actorless and multiple-file uploads before provider calls', async () => { + await expect( + executeOraclePcmFileOperation('upload_file', { + ...auth, + fileName: 'data.csv', + file, + userId: 'forged', + }) + ).rejects.toThrow('actor') + await expect( + executeOraclePcmFileOperation( + 'upload_file', + { ...auth, fileName: 'data.csv', file: [file, file] }, + undefined, + context + ) + ).rejects.toThrow('exactly one') + expect(mocks.open).not.toHaveBeenCalled() + expect(mocks.fetch).not.toHaveBeenCalled() + }) + it('honors denied source access before uploading', async () => { + mocks.open.mockRejectedValue(new Error('File access denied')) + await expect( + executeOraclePcmFileOperation( + 'upload_file', + { ...auth, fileName: 'data.csv', file }, + undefined, + context + ) + ).rejects.toThrow('denied') + expect(mocks.fetch).not.toHaveBeenCalled() + }) + it('checks actual upload bytes even when source metadata is smaller', async () => { + const chunk = Buffer.alloc(1024 * 1024) + mocks.open.mockResolvedValue({ + fileName: 'report.csv', + contentType: 'text/csv', + maxBytes: PCM_MAX_TRANSFER_BYTES, + chunks: (async function* () { + for (let index = 0; index < 101; index++) yield chunk + })(), + }) + await expect( + executeOraclePcmFileOperation( + 'upload_file', + { ...auth, fileName: 'data.csv', file }, + undefined, + context + ) + ).rejects.toThrow('maximum size') + expect(mocks.fetch).not.toHaveBeenCalled() + }) + it.each([1, -1])( + 'does not overwrite, delete, retry, or follow snapshot processing on status %s', + async (status) => { + mocks.fetch.mockImplementation(() => json({ status, details: 'provider detail' })) + await expect( + executeOraclePcmFileOperation( + 'upload_file', + { ...auth, fileName: 'data.zip', file }, + undefined, + context + ) + ).rejects.toThrow() + expect(mocks.fetch).toHaveBeenCalledTimes(1) + expect(mocks.fetch.mock.calls[0][2].method).toBe('POST') + } + ) + it('stores a downloaded stream as a canonical UserFile with trusted scope and caps', async () => { + mocks.fetch + .mockImplementationOnce(() => listing()) + .mockImplementationOnce( + () => + new Response('abc', { headers: { 'content-type': 'text/csv', 'content-length': '3' } }) + ) + const signal = new AbortController().signal + const result = await executeOraclePcmFileOperation( + 'download_file', + { ...auth, fileName: 'profitoutbox/report final.csv', workspaceId: 'forged' }, + signal, + context + ) + expect(result.output).toEqual({ file }) + expect(mocks.store).toHaveBeenCalledWith( + expect.objectContaining({ + fileName: 'report final.csv', + maxBytes: PCM_MAX_TRANSFER_BYTES, + contentLength: 3, + signal, + context: { + workspaceId: context.workspaceId, + workflowId: context.workflowId, + executionId: context.executionId, + }, + }) + ) + expect(mocks.fetch.mock.calls[1][0]).toContain('/profitoutbox%2Freport%20final.csv/contents') + }) + it('does not save Oracle JSON download errors as successful files', async () => { + mocks.fetch + .mockImplementationOnce(() => listing()) + .mockImplementationOnce(() => json({ status: 1, details: 'Invalid file' })) + await expect( + executeOraclePcmFileOperation( + 'download_file', + { ...auth, fileName: 'profitoutbox/report final.csv' }, + undefined, + context + ) + ).rejects.toThrow('no file was stored') + expect(mocks.store).not.toHaveBeenCalled() + }) + it('rejects oversized downloads from repository metadata', async () => { + mocks.fetch.mockImplementationOnce(() => + listing('profitoutbox/data.csv', String(PCM_MAX_TRANSFER_BYTES + 1)) + ) + await expect( + executeOraclePcmFileOperation( + 'download_file', + { ...auth, fileName: 'profitoutbox/data.csv' }, + undefined, + context + ) + ).rejects.toThrow('100 MiB') + expect(mocks.fetch).toHaveBeenCalledTimes(1) + }) + it('does not let manual names target snapshots or unrelated repository files', async () => { + mocks.fetch.mockImplementation(() => + json({ + status: 0, + items: [ + { name: 'profitoutbox/snapshot', type: 'LCM', size: null, lastmodifiedtime: null }, + { name: 'inbox/data.csv', type: 'EXTERNAL', size: '3', lastmodifiedtime: '0' }, + ], + }) + ) + for (const fileName of ['profitoutbox/snapshot', 'inbox/data.csv']) { + await expect( + executeOraclePcmFileOperation('download_file', { ...auth, fileName }, undefined, context) + ).rejects.toThrow('not found') + } + expect(mocks.store).not.toHaveBeenCalled() + expect(mocks.fetch.mock.calls.every((call) => call[2].method === 'GET')).toBe(true) + }) + + it('lists only PCM ordinary files, preserving long and whitespace-sensitive names', async () => { + const longName = `profitinbox/${'x'.repeat(300)}` + mocks.fetch.mockImplementation(() => + json({ + status: 0, + items: [ + { name: longName, type: 'EXTERNAL', size: '3', lastmodifiedtime: '0' }, + { name: 'profitoutbox/report.csv ', type: 'EXTERNAL', size: '3', lastmodifiedtime: '0' }, + { name: 'inbox/other.csv', type: 'EXTERNAL', size: '3', lastmodifiedtime: '0' }, + { name: 'Snapshot', type: 'LCM', size: null, lastmodifiedtime: null }, + ], + }) + ) + expect(await executeOraclePcmFileOperation('list_files', auth)).toMatchObject({ + output: { + files: [ + { name: longName, size: 3, lastModifiedTime: 0 }, + { name: 'profitoutbox/report.csv ', size: 3, lastModifiedTime: 0 }, + ], + }, + }) + expect(mocks.fetch.mock.calls[0][0]).toBe( + 'https://epm.example.com/gateway/interop/rest/v2/files/list' + ) + }) + + it('rejects UTF-8 paths beyond the download boundary before source access or HTTP', async () => { + const name = `${'é'.repeat(123)}.csv` + await expect( + executeOraclePcmFileOperation( + 'upload_file', + { ...auth, fileName: name, file }, + undefined, + context + ) + ).rejects.toThrow('255 UTF-8 bytes') + await expect( + executeOraclePcmFileOperation( + 'download_file', + { ...auth, fileName: `profitoutbox/${name}` }, + undefined, + context + ) + ).rejects.toThrow('255 UTF-8 bytes') + expect(mocks.open).not.toHaveBeenCalled() + expect(mocks.fetch).not.toHaveBeenCalled() + }) + + it('propagates cancellation without opening files or calling Oracle', async () => { + const controller = new AbortController() + controller.abort() + await expect( + executeOraclePcmFileOperation( + 'upload_file', + { ...auth, fileName: 'data.csv', file }, + controller.signal, + context + ) + ).rejects.toThrow() + expect(mocks.open).not.toHaveBeenCalled() + expect(mocks.fetch).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm-profitability/files.server.ts b/apps/sim/lib/internal/oracle-epm-profitability/files.server.ts new file mode 100644 index 00000000000..0eaba4b2917 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/files.server.ts @@ -0,0 +1,190 @@ +import { Readable } from 'node:stream' +import { z } from 'zod' +import { + readNodeStreamToBufferWithLimit, + readStreamToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' +import { isUserFileWithMetadata } from '@/lib/core/utils/user-file' +import { + defineOracleEpmRouteSpace, + oracleEpmLiteral as literal, + openOracleEpmSourceFile, + oracleEpmQuery, + oracleEpmPathParameter as parameter, + storeOracleEpmDownload, +} from '@/lib/internal/oracle-epm' +import { + normalizeOraclePcmFiles, + OraclePcmOperationError, + PCM_MAX_JSON_BYTES, + pcmFileName, + pcmRepositoryName, + requireOraclePcmDownloadablePath, + requireOraclePcmRepositorySuccess, +} from '@/lib/internal/oracle-epm-profitability/normalizers' +import { + oraclePcmClient, + parseOraclePcmInput, + pcmAuthSchema, + requestOraclePcmJson, +} from '@/lib/internal/oracle-epm-profitability/operations' +import type { InternalToolOperationContext } from '@/lib/internal/tool-operations/types' +import type { UserFile } from '@/executor/types' +import type { OraclePcmResponse } from '@/tools/oracle_epm_profitability/types' + +export const PCM_MAX_TRANSFER_BYTES = 100 * 1024 * 1024 +const interop = defineOracleEpmRouteSpace({ + context: ['interop', 'rest'], + allowedVersions: ['v2', '11.1.2.3.600'], +}) +const listEndpoint = interop.defineEndpoint({ + method: 'GET', + version: 'v2', + path: [literal('files'), literal('list')], + body: 'none', + response: 'json', + timeoutMs: 30_000, + maxResponseBytes: PCM_MAX_JSON_BYTES, +}) +const contentsPath = [ + literal('applicationsnapshots'), + parameter('fileName', { maxBytes: 255, mode: 'repository-path' }), + literal('contents'), +] +const uploadEndpoint = interop.defineEndpoint({ + method: 'POST', + version: '11.1.2.3.600', + path: contentsPath, + body: 'stream', + response: 'json', + timeoutMs: 300_000, + maxRequestBytes: PCM_MAX_TRANSFER_BYTES, + query: { + extDirPath: oracleEpmQuery.string({ required: true, maxBytes: 11, pattern: /^profitinbox$/ }), + }, + maxResponseBytes: PCM_MAX_JSON_BYTES, +}) +const downloadEndpoint = interop.defineEndpoint({ + method: 'GET', + version: '11.1.2.3.600', + path: contentsPath, + body: 'none', + response: 'stream', + timeoutMs: 300_000, + maxResponseBytes: PCM_MAX_TRANSFER_BYTES, +}) +const fileSchema = pcmAuthSchema.extend({ fileName: pcmRepositoryName }) + +export async function listOraclePcmFiles(input: unknown, signal?: AbortSignal) { + const params = parseOraclePcmInput(pcmAuthSchema, input) + return normalizeOraclePcmFiles( + await requestOraclePcmJson(oraclePcmClient(params), listEndpoint, { signal }) + ) +} + +function requireSourceFile(value: unknown): UserFile { + const file = Array.isArray(value) && value.length === 1 ? value[0] : value + if (!isUserFileWithMetadata(file)) { + throw new OraclePcmOperationError('Upload requires exactly one canonical Sim UserFile') + } + return file +} + +export async function executeOraclePcmFileOperation( + operation: string, + input: unknown, + signal?: AbortSignal, + context?: InternalToolOperationContext +): Promise { + signal?.throwIfAborted() + if (operation === 'list_files') { + return { success: true, output: { files: await listOraclePcmFiles(input, signal) } } + } + const params = parseOraclePcmInput( + operation === 'upload_file' ? fileSchema.extend({ fileName: pcmFileName }) : fileSchema, + input + ) + const client = oraclePcmClient(params) + if (operation === 'upload_file') { + requireOraclePcmDownloadablePath(`profitinbox/${params.fileName}`) + const { file } = parseOraclePcmInput(fileSchema.extend({ file: z.unknown() }), input) + if (!context?.userId) + throw new OraclePcmOperationError('Uploading requires an authenticated execution actor') + const source = await openOracleEpmSourceFile({ + file: requireSourceFile(file), + userId: context.userId, + maxBytes: PCM_MAX_TRANSFER_BYTES, + signal, + }) + const body = await readNodeStreamToBufferWithLimit(Readable.from(source.chunks), { + maxBytes: PCM_MAX_TRANSFER_BYTES, + label: 'Oracle PCM upload', + signal, + }) + const data = await requestOraclePcmJson(client, uploadEndpoint, { + pathParams: { fileName: params.fileName }, + query: { extDirPath: 'profitinbox' }, + stream: body, + signal, + }) + requireOraclePcmRepositorySuccess(data) + return { + success: true, + output: { fileName: `profitinbox/${params.fileName}`, status: 0 }, + retryable: false, + } + } + if (operation !== 'download_file') { + throw new OraclePcmOperationError('Unsupported Oracle PCM file operation') + } + requireOraclePcmDownloadablePath(params.fileName) + const executionContext = + operation === 'download_file' + ? parseOraclePcmInput( + z.object({ + workspaceId: z.string().uuid(), + workflowId: z.string().uuid(), + executionId: z.string().uuid(), + }), + context + ) + : undefined + /** Ordinary-file operations cannot target a listed LCM snapshot, even through a manual name. */ + const ordinaryFile = (await listOraclePcmFiles(params, signal)).find( + (file) => file.name === params.fileName + ) + if (!ordinaryFile) + throw new OraclePcmOperationError( + 'The ordinary repository file was not found; snapshots are not supported', + 404 + ) + if (ordinaryFile.size !== null && ordinaryFile.size > PCM_MAX_TRANSFER_BYTES) { + throw new OraclePcmOperationError('The repository file exceeds the 100 MiB transfer limit', 413) + } + const response = await client.request(downloadEndpoint, { + pathParams: { fileName: params.fileName }, + signal, + }) + if (!('body' in response)) + throw new OraclePcmOperationError('Oracle returned an invalid download response', 502) + if (response.contentType?.split(';')[0]?.trim().toLowerCase() === 'application/json') { + await readStreamToBufferWithLimit(response.body, { + maxBytes: 65_536, + label: 'Oracle PCM download error', + signal, + }) + throw new OraclePcmOperationError('Oracle rejected the file download; no file was stored', 502) + } + if (!executionContext) + throw new OraclePcmOperationError('A trusted execution file context is required') + const file = await storeOracleEpmDownload({ + body: response.body, + contentLength: response.contentLength, + contentType: response.contentType, + fileName: params.fileName.split('/').at(-1) || params.fileName, + context: executionContext, + maxBytes: PCM_MAX_TRANSFER_BYTES, + signal, + }) + return { success: true, output: { file } } +} diff --git a/apps/sim/lib/internal/oracle-epm-profitability/jobs.test.ts b/apps/sim/lib/internal/oracle-epm-profitability/jobs.test.ts new file mode 100644 index 00000000000..4083b94c5f8 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/jobs.test.ts @@ -0,0 +1,138 @@ +/** @vitest-environment node */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ fetch: vi.fn(), dns: vi.fn() })) +vi.mock('@/lib/core/security/input-validation.server', () => ({ + DEFAULT_MAX_RESPONSE_BYTES: 100 * 1024 * 1024, + secureFetchWithPinnedIP: mocks.fetch, + validateUrlWithDNS: mocks.dns, +})) +vi.mock('@/lib/internal/oracle-epm/files.server', () => ({ + openOracleEpmSourceFile: vi.fn(), + storeOracleEpmDownload: vi.fn(), +})) + +import { oracleEpmLocalError } from '@/lib/internal/oracle-epm/errors' +import { executeOraclePcmJobOperation } from '@/lib/internal/oracle-epm-profitability/jobs' + +const params = { + oauthCredential: 'credential-1', + accessToken: Buffer.from('test-user:test-password').toString('base64'), + instanceUrl: 'https://epm.example.com/gateway', + processName: 'Task-1', +} +const json = (value: unknown) => + new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } }) + +describe('Oracle PCM task monitoring', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.dns.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' }) + mocks.fetch.mockImplementation(() => json({ status: 0, details: null })) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it.each([ + [-1, 'pending'], + [0, 'succeeded'], + [1, 'failed'], + [2, 'failed'], + [99, 'failed'], + ])('interprets migration status %s as %s', async (status, state) => { + mocks.fetch.mockImplementation(() => json({ status, details: null })) + expect(await executeOraclePcmJobOperation('get_task_status', params)).toMatchObject({ + success: true, + output: { processName: 'Task-1', status, state }, + }) + expect(mocks.fetch.mock.calls[0][0]).toBe( + 'https://epm.example.com/gateway/epm/rest/v1/applications/jobs/ChecktaskStatusJob/Task-1' + ) + expect(mocks.fetch.mock.calls[0][2].method).toBe('GET') + expect(mocks.fetch).toHaveBeenCalledTimes(1) + }) + + it('polls pending to success using reads only', async () => { + vi.useFakeTimers() + mocks.fetch.mockImplementationOnce(() => json({ status: -1 })) + const result = executeOraclePcmJobOperation('wait_for_task', params) + await vi.advanceTimersByTimeAsync(2_000) + expect(await result).toMatchObject({ + success: true, + retryable: false, + output: { processName: 'Task-1', status: 0, attempts: 2, timedOut: false }, + }) + expect(mocks.fetch.mock.calls.every((call) => call[2].method === 'GET')).toBe(true) + }) + + it('stops immediately on positive status without replaying the mutation', async () => { + mocks.fetch.mockImplementation(() => json({ status: 2 })) + expect(await executeOraclePcmJobOperation('wait_for_task', params)).toMatchObject({ + success: false, + retryable: false, + output: { processName: 'Task-1', state: 'failed', attempts: 1, timedOut: false }, + }) + expect(mocks.fetch).toHaveBeenCalledTimes(1) + }) + + it('returns the last task and identifier when the bounded wait expires', async () => { + vi.useFakeTimers() + mocks.fetch.mockImplementation(() => json({ status: -1, details: 'Running' })) + const result = executeOraclePcmJobOperation('wait_for_task', { ...params, maxWaitSeconds: 1 }) + await vi.advanceTimersByTimeAsync(1_000) + expect(await result).toMatchObject({ + success: false, + retryable: false, + output: { processName: 'Task-1', timedOut: true, status: -1, details: 'Running' }, + }) + expect(mocks.fetch.mock.calls.every((call) => call[2].method === 'GET')).toBe(true) + }) + + it('retains the identifier on a transport timeout before the first status response', async () => { + mocks.fetch.mockRejectedValue(oracleEpmLocalError('timeout')) + expect(await executeOraclePcmJobOperation('wait_for_task', params)).toMatchObject({ + success: false, + retryable: false, + output: { processName: 'Task-1', timedOut: true }, + }) + }) + + it('propagates cancellation without cancelling the remote task', async () => { + vi.useFakeTimers() + mocks.fetch.mockImplementation(() => json({ status: -1 })) + const controller = new AbortController() + const result = expect( + executeOraclePcmJobOperation('wait_for_task', params, controller.signal) + ).rejects.toMatchObject({ name: 'AbortError' }) + await vi.advanceTimersByTimeAsync(1) + controller.abort() + await result + expect(mocks.fetch.mock.calls.every((call) => call[2].method === 'GET')).toBe(true) + }) + + it.each(['', '../Task-1', 'a/b'])( + 'rejects invalid task references before HTTP', + async (processName) => { + await expect( + executeOraclePcmJobOperation('get_task_status', { ...params, processName }) + ).rejects.toThrow() + expect(mocks.fetch).not.toHaveBeenCalled() + } + ) + + it('rejects fractional wait budgets before invoking the polling scheduler', async () => { + await expect( + executeOraclePcmJobOperation('wait_for_task', { ...params, maxWaitSeconds: 1.0001 }) + ).rejects.toThrow('maxWaitSeconds') + expect(mocks.fetch).not.toHaveBeenCalled() + }) + + it('rejects undocumented task response shapes', async () => { + mocks.fetch.mockImplementation(() => json({ status: '0', processName: 'invented' })) + await expect(executeOraclePcmJobOperation('get_task_status', params)).rejects.toThrow( + 'contract' + ) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm-profitability/jobs.ts b/apps/sim/lib/internal/oracle-epm-profitability/jobs.ts new file mode 100644 index 00000000000..76ec127b815 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/jobs.ts @@ -0,0 +1,88 @@ +import { z } from 'zod' +import { OracleEpmError, pollOracleEpmJob } from '@/lib/internal/oracle-epm' +import { + normalizeOraclePcmTask, + OraclePcmOperationError, + pcmName, +} from '@/lib/internal/oracle-epm-profitability/normalizers' +import { + oraclePcmClient, + parseOraclePcmInput, + pcmAuthSchema, + pcmNumber, + pcmTaskEndpoint, + requestOraclePcmJson, +} from '@/lib/internal/oracle-epm-profitability/operations' +import type { OraclePcmResponse, OraclePcmTask } from '@/tools/oracle_epm_profitability/types' + +export async function executeOraclePcmJobOperation( + operation: string, + input: unknown, + signal?: AbortSignal +): Promise { + const params = parseOraclePcmInput( + pcmAuthSchema.extend({ + processName: pcmName, + maxWaitSeconds: pcmNumber(1, 3_600, 300).pipe(z.number().int()), + }), + input + ) + const client = oraclePcmClient(params) + const read = async (readSignal?: AbortSignal) => + normalizeOraclePcmTask( + await requestOraclePcmJson(client, pcmTaskEndpoint, { + pathParams: { processName: params.processName }, + signal: readSignal, + }), + params.processName + ) + if (operation === 'get_task_status') return { success: true, output: await read(signal) } + if (operation !== 'wait_for_task') + throw new OraclePcmOperationError('Unsupported Oracle PCM task operation') + let lastTask: OraclePcmTask | undefined + try { + const result = await pollOracleEpmJob({ + read: async (pollSignal) => { + lastTask = await read(pollSignal) + return lastTask + }, + classify: (task) => + task.state === 'pending' + ? { state: 'pending' } + : task.state === 'succeeded' + ? { state: 'success', result: task } + : { state: 'failure', error: task }, + signal, + maxWaitMs: (params.maxWaitSeconds ?? 300) * 1_000, + cleanupReserveMs: 100, + maxAttempts: 1_000, + initialDelayMs: 1_000, + maxDelayMs: 10_000, + }) + return { + success: result.state === 'success', + output: { + ...(result.state === 'success' ? result.result : result.error), + attempts: result.attempts, + timedOut: false, + }, + retryable: false, + ...(result.state === 'failure' + ? { error: 'Oracle PCM task failed; it was not resubmitted' } + : {}), + } + } catch (error) { + signal?.throwIfAborted() + const timedOut = + (error instanceof Error && error.name === 'TimeoutError') || + (error instanceof OracleEpmError && error.category === 'timeout') + if (!timedOut) throw error + return { + success: false, + output: { ...lastTask, processName: params.processName, timedOut: true }, + error: + 'Oracle PCM wait timed out; the remote task was not cancelled. Resume with the same processName.', + retryable: false, + } + } +} diff --git a/apps/sim/lib/internal/oracle-epm-profitability/normalizers.ts b/apps/sim/lib/internal/oracle-epm-profitability/normalizers.ts new file mode 100644 index 00000000000..4b8563c1d7b --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/normalizers.ts @@ -0,0 +1,122 @@ +import { z } from 'zod' +import type { OraclePcmRepositoryFile, OraclePcmTask } from '@/tools/oracle_epm_profitability/types' + +export const PCM_MAX_JSON_BYTES = 4 * 1024 * 1024 +export const PCM_MAX_ITEMS = 10_000 +export const pcmName = z.string().trim().min(1).max(255) +export const pcmRepositoryName = z.string().min(1).max(4_096) +export const pcmFileName = pcmRepositoryName + .max(255) + .regex(/^[^/\\]+$/, 'Use a filename without repository folders') + +export function isOraclePcmDownloadablePath(name: string): boolean { + return new TextEncoder().encode(name).byteLength <= 255 +} + +export function requireOraclePcmDownloadablePath(name: string): void { + if (!isOraclePcmDownloadablePath(name)) { + throw new OraclePcmOperationError('PCM repository paths must fit within 255 UTF-8 bytes') + } +} + +export class OraclePcmOperationError extends Error { + constructor( + message: string, + readonly status = 400 + ) { + super(message) + this.name = 'OraclePcmOperationError' + } +} + +export function requireOraclePcmResponse(schema: z.ZodType, value: unknown): T { + const result = schema.safeParse(value) + if (!result.success) { + throw new OraclePcmOperationError('Oracle PCM returned an invalid response contract', 502) + } + return result.data +} + +export const pcmStatusSchema = z.object({ + status: z.number().int().min(-1), + statusMessage: z.string().max(65_536).nullish(), + details: z.string().max(65_536).nullish(), +}) + +/** PCM uses migration statuses: every positive code is a failure, including 2. */ +export function normalizeOraclePcmTask(value: unknown, processName: string | null): OraclePcmTask { + const data = requireOraclePcmResponse(pcmStatusSchema, value) + return { + processName, + status: data.status, + state: data.status === -1 ? 'pending' : data.status === 0 ? 'succeeded' : 'failed', + statusMessage: data.statusMessage ?? null, + details: data.details ?? null, + } +} + +export function requireOraclePcmRepositorySuccess(value: unknown): void { + const data = requireOraclePcmResponse(z.object({ status: z.number().int() }), value) + if (data.status !== 0) { + throw new OraclePcmOperationError( + data.status === -1 + ? 'Oracle started unsupported snapshot processing; the upload was not retried' + : 'Oracle rejected the repository operation; check the filename and permissions', + 502 + ) + } +} + +const decimalMetadata = z + .union([z.number().int().nonnegative().safe(), z.string().regex(/^\d{1,16}$/)]) + .transform(Number) + .refine(Number.isSafeInteger) + .nullable() + +export function normalizeOraclePcmFiles(value: unknown): OraclePcmRepositoryFile[] { + requireOraclePcmRepositorySuccess(value) + const data = requireOraclePcmResponse( + z.object({ + items: z + .array( + z.object({ + name: pcmRepositoryName, + type: z.enum(['EXTERNAL', 'LCM']), + size: decimalMetadata, + lastmodifiedtime: decimalMetadata, + }) + ) + .max(PCM_MAX_ITEMS), + }), + value + ) + return data.items + .filter((item) => item.type === 'EXTERNAL' && /^(profitinbox|profitoutbox)\//.test(item.name)) + .map((item) => ({ + name: item.name, + type: 'EXTERNAL', + size: item.size, + lastModifiedTime: item.lastmodifiedtime, + })) +} + +/** Oracle documents scalar balance fields but not the contents of the nested rules array. */ +export const pcmBalanceSchema = z.object({ + ruleNumber: z.string().max(255), + balanceTypeRule: z.boolean(), + scale: z.number(), + sequence: z.number(), + name: pcmName, + description: z.string().max(65_536).nullable(), + runningBalance: z.number().nullable(), + balance: z.number().nullable(), + allocationIn: z.number().nullable(), + allocationOut: z.number().nullable(), + adjustmentIn: z.number().nullable(), + adjustmentOut: z.number().nullable(), + input: z.number().nullable(), + runningRemainder: z.number().nullable(), + remainder: z.number().nullable(), + netChange: z.number().nullable(), + offset: z.number().nullable(), +}) diff --git a/apps/sim/lib/internal/oracle-epm-profitability/operations.test.ts b/apps/sim/lib/internal/oracle-epm-profitability/operations.test.ts new file mode 100644 index 00000000000..92b36832117 --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/operations.test.ts @@ -0,0 +1,318 @@ +/** @vitest-environment node */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { OracleEpmClient } from '@/lib/internal/oracle-epm' +import { getOracleEpmEndpoint } from '@/lib/internal/oracle-epm/endpoint' + +const { request, validateReturnedLink } = vi.hoisted(() => ({ + request: vi.fn(), + validateReturnedLink: vi.fn(), +})) +vi.mock('@/lib/internal/oracle-epm/client.server', () => ({ + createOracleEpmClient: () => ({ request, validateReturnedLink }), +})) +vi.mock('@/lib/internal/oracle-epm/files.server', () => ({ + openOracleEpmSourceFile: vi.fn(), + storeOracleEpmDownload: vi.fn(), +})) + +import { + executeOraclePcmOperation, + normalizeOraclePcmSubmission, +} from '@/lib/internal/oracle-epm-profitability/operations' + +const auth = { + oauthCredential: 'credential', + accessToken: 'token', + instanceUrl: 'https://example.com', + applicationName: 'Model', +} +const submission = { + status: -1, + statusMessage: 'In Progress', + details: 'Task_1', + links: [ + { + rel: 'Job Status', + action: 'GET', + href: 'https://example.com/epm/rest/v1/applications/jobs/ChecktaskStatusJob/Task_1', + }, + ], +} + +const cases = [ + [ + 'create_application', + { description: 'Allocation', ruleDimensionName: 'Rule', balanceDimensionName: 'Balance' }, + 'fileApplications/:applicationName', + { description: 'Allocation', ruleDimensionName: 'Rule', balanceDimensionName: 'Balance' }, + ], + ['enable_application', {}, 'fileApplications/:applicationName/enableApplication', {}], + [ + 'deploy_cube', + { isKeepData: true, isReplaceCube: false, comment: 'Deploy' }, + 'applications/:applicationName/jobs/ledgerDeployCubeJob', + { isKeepData: 'true', isReplaceCube: 'false', comment: 'Deploy', isRunNow: 'true' }, + ], + [ + 'update_dimensions', + { dataFileName: 'Account.csv,Product.csv', acceptableDecreasePercentage: 5 }, + 'fileApplications/:applicationName/jobs/updateDimension', + { dataFileName: 'Account.csv,Product.csv', acceptableDecreasePercentage: '5' }, + ], + [ + 'load_data', + { clearAllDataFlag: false, dataLoadValue: 'ADD_EXISTING_VALUES', dataFileName: 'Data.csv' }, + 'applications/:applicationName/jobs/essbaseDataLoadJob', + { clearAllDataFlag: 'false', dataLoadValue: 'ADD_EXISTING_VALUES', dataFileName: 'Data.csv' }, + ], + [ + 'run_calculation', + { povName: 'FY26_Jan_Actual', exeType: 'ALL_RULES' }, + 'applications/:applicationName/povs/:povName/jobs/runLedgerCalculationJob', + { exeType: 'ALL_RULES', isRunNow: 'true', isExecuteCalculations: 'true' }, + ], + [ + 'copy_pov', + { + povName: 'Source', + destinationPovName: 'Target', + isManageRule: true, + isInputData: false, + createDestPOV: true, + stringDelimiter: '_', + }, + 'applications/:applicationName/povs/:povName/jobs/copyPOVJob/:destinationPovName', + { isManageRule: 'true', isInputData: 'false', createDestPOV: 'true', stringDelimiter: '_' }, + ], + [ + 'clear_pov', + { povName: 'FY26_Jan_Actual', isInputData: true, queryName: 'Input' }, + 'applications/:applicationName/povs/:povName/jobs/clearPOVJob', + { isInputData: 'true', queryName: 'Input' }, + ], + [ + 'generate_program_documentation', + { povName: 'FY26_Jan_Actual', fileName: 'Program.pdf', skipFilters: true }, + 'applications/:applicationName/povs/:povName/jobs/programDocReportJob', + { fileName: 'Program.pdf', skipFilters: true }, + ], + [ + 'export_query_results', + { fileName: 'Results.txt', queryName: 'Profitability', roundingPrecision: 3 }, + 'applications/:applicationName/jobs/exportQueryResultsJob', + { fileName: 'Results.txt', queryName: 'Profitability', roundingPrecision: '3' }, + ], + [ + 'import_template', + { description: 'Import', fileName: 'Template.zip', isApplicationOverwrite: false }, + 'applications/:applicationName/jobs/templateImportJob', + { description: 'Import', fileName: 'Template.zip', isApplicationOverwrite: 'false' }, + ], + ['apply_data_grants', {}, 'applications/:applicationName/jobs/applyDataGrants', {}], + [ + 'merge_slices', + { removeZeroCells: true }, + 'applications/:applicationName/jobs/mergeSlices', + { removeZeroCells: 'true' }, + ], + [ + 'optimize_cube', + { type: 'createAggregations' }, + 'applications/:applicationName/jobs/optimizeASOCube', + { type: 'createAggregations' }, + ], +] as const + +describe('PCM documented operations', () => { + beforeEach(() => { + vi.clearAllMocks() + request.mockResolvedValue({ status: 200, data: submission }) + }) + + it.each(cases)('%s uses its PCM endpoint and payload', async (operation, params, path, json) => { + const signal = new AbortController().signal + const result = await executeOraclePcmOperation(operation, { ...auth, ...params }, signal) + const [endpoint, input] = request.mock.calls[0] + const declaration = getOracleEpmEndpoint(endpoint) + expect(declaration.routeSpace.context).toEqual(['epm', 'rest']) + expect(declaration.version).toBe('v1') + expect(declaration.method).toBe('POST') + expect( + declaration.path + .map((part) => (part.kind === 'literal' ? part.value : `:${part.name}`)) + .join('/') + ).toBe(path) + expect(input.json).toEqual(json) + expect(input.pathParams.applicationName).toBe('Model') + expect(input.signal).toBe(signal) + expect(input.query).toBeUndefined() + expect(result.output).toMatchObject({ processName: 'Task_1', state: 'pending' }) + expect(result.retryable).toBe(false) + }) + + it.each([ + ['deploy_cube', { isKeepData: true, isReplaceCube: true, comment: 'Deploy' }], + ['run_calculation', { povName: 'POV', exeType: 'SINGLE_RULE' }], + [ + 'run_calculation', + { povName: 'POV', exeType: 'RULESET_SUBSET', subsetStart: 9, subsetEnd: 1 }, + ], + [ + 'run_calculation', + { + povName: 'POV', + exeType: 'SINGLE_RULE', + ruleName: 'Rule', + ruleSetName: 'Set', + dataPOVName: 'Other', + }, + ], + ['clear_pov', { povName: 'POV', queryName: 'Input', isManageRule: true }], + ])('rejects incompatible %s inputs before provider access', async (operation, params) => { + await expect( + executeOraclePcmOperation(operation as string, { ...auth, ...params }) + ).rejects.toThrow() + expect(request).not.toHaveBeenCalled() + }) + + it('drops irrelevant rule-selection fields and blank optional inputs', async () => { + await executeOraclePcmOperation('run_calculation', { + ...auth, + povName: 'POV', + exeType: 'ALL_RULES', + subsetStart: 1, + ruleName: 'Unused', + comment: '', + }) + expect(request.mock.calls[0][1].json).toEqual({ + exeType: 'ALL_RULES', + isRunNow: 'true', + isExecuteCalculations: 'true', + }) + }) + + it('sends columnar memberFilters as the documented JSON string', async () => { + const memberFilters = '{"Product":["P1"]}' + await executeOraclePcmOperation('export_query_results', { + ...auth, + fileName: 'Export.txt', + dataFormat: 'COLUMNAR', + memberFilters, + }) + expect(request.mock.calls[0][1].json.memberFilters).toBe(memberFilters) + request.mockClear() + await expect( + executeOraclePcmOperation('export_query_results', { + ...auth, + fileName: 'Export.txt', + memberFilters: '{invalid', + }) + ).rejects.toThrow('memberFilters') + expect(request).not.toHaveBeenCalled() + }) + + it('uses queryParameter for rule balancing and projects documented scalar fields only', async () => { + const item = { + ruleNumber: '', + balanceTypeRule: true, + scale: 2, + sequence: 0, + name: 'NoRule', + description: null, + runningBalance: 10, + balance: 10, + allocationIn: null, + allocationOut: null, + adjustmentIn: null, + adjustmentOut: null, + input: 10, + runningRemainder: 10, + remainder: 10, + netChange: null, + offset: null, + rules: [{ undocumented: 'ignored' }], + } + request.mockResolvedValue({ status: 200, data: { status: 0, items: [item] } }) + const result = await executeOraclePcmOperation('get_rule_balancing', { + ...auth, + povName: 'POV', + modelViewName: 'View', + stringDelimiter: '_', + }) + expect(request.mock.calls[0][1].query).toEqual({ + queryParameter: '{"modelViewName":"View","stringDelimiter":"_"}', + }) + expect(result.output).toMatchObject({ items: [{ name: 'NoRule', balance: 10 }] }) + expect('items' in result.output && result.output.items[0]).not.toHaveProperty('rules') + }) + + it('accepts nonnegative integer precision without inventing an Oracle maximum', async () => { + await executeOraclePcmOperation('export_query_results', { + ...auth, + fileName: 'Export.txt', + queryName: 'Profitability', + roundingPrecision: 16, + }) + expect(request.mock.calls[0][1].json.roundingPrecision).toBe('16') + request.mockClear() + await expect( + executeOraclePcmOperation('export_query_results', { + ...auth, + fileName: 'Export.txt', + roundingPrecision: 1.5, + }) + ).rejects.toThrow('roundingPrecision') + expect(request).not.toHaveBeenCalled() + }) + + it('takes report processName from the validated link, never descriptive details', async () => { + request.mockResolvedValue({ + data: { ...submission, details: 'Report Program.pdf generated in the Outbox folder.' }, + }) + const result = await executeOraclePcmOperation('generate_program_documentation', { + ...auth, + povName: 'POV', + }) + expect(result.output).toMatchObject({ + processName: 'Task_1', + details: 'Report Program.pdf generated in the Outbox folder.', + }) + expect(validateReturnedLink).toHaveBeenCalledWith(expect.anything(), { + rel: 'Job Status', + method: 'GET', + href: submission.links[0].href, + }) + }) + + it('rejects missing or ambiguous pending-task links and propagates foundation link rejection', () => { + const client = { + request, + validateReturnedLink, + requestValidatedLink: vi.fn(), + } as OracleEpmClient + expect(() => normalizeOraclePcmSubmission({ ...submission, links: [] }, client)).toThrow( + 'unique task' + ) + expect(() => + normalizeOraclePcmSubmission( + { ...submission, links: [...submission.links, ...submission.links] }, + client + ) + ).toThrow('unique task') + validateReturnedLink.mockImplementationOnce(() => { + throw new Error('Rejected route') + }) + expect(() => normalizeOraclePcmSubmission(submission, client)).toThrow('Rejected route') + }) + + it('reports positive submission statuses as failure without retrying', async () => { + request.mockResolvedValue({ data: { status: 2, details: 'Failed' } }) + const result = await executeOraclePcmOperation('apply_data_grants', auth) + expect(result).toMatchObject({ + success: false, + retryable: false, + output: { status: 2, state: 'failed', processName: null }, + }) + expect(request).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/sim/lib/internal/oracle-epm-profitability/operations.ts b/apps/sim/lib/internal/oracle-epm-profitability/operations.ts new file mode 100644 index 00000000000..3b3779d989c --- /dev/null +++ b/apps/sim/lib/internal/oracle-epm-profitability/operations.ts @@ -0,0 +1,416 @@ +import { filterUndefined, isPlainRecord, omit } from '@sim/utils/object' +import { z } from 'zod' +import { + createOracleEpmClient, + defineOracleEpmRouteSpace, + oracleEpmLiteral as literal, + type OracleEpmClient, + type OracleEpmEndpoint, + oracleEpmQuery, + oracleEpmPathParameter as parameter, +} from '@/lib/internal/oracle-epm' +import { + normalizeOraclePcmTask, + OraclePcmOperationError, + PCM_MAX_ITEMS, + PCM_MAX_JSON_BYTES, + pcmBalanceSchema, + pcmFileName, + pcmName, + pcmStatusSchema, + requireOraclePcmResponse, +} from '@/lib/internal/oracle-epm-profitability/normalizers' +import type { OraclePcmResponse, OraclePcmTask } from '@/tools/oracle_epm_profitability/types' +import { parseOraclePcmBoolean } from '@/tools/oracle_epm_profitability/utils' + +export const pcmRoutes = defineOracleEpmRouteSpace({ + context: ['epm', 'rest'], + allowedVersions: ['v1'], +}) +export const pcmJsonPolicy = { + version: 'v1', + response: 'json', + timeoutMs: 30_000, + maxResponseBytes: PCM_MAX_JSON_BYTES, +} as const +export const pcmTaskEndpoint = pcmRoutes.defineEndpoint({ + ...pcmJsonPolicy, + method: 'GET', + body: 'none', + path: [ + literal('applications'), + literal('jobs'), + literal('ChecktaskStatusJob'), + parameter('processName', { maxBytes: 255 }), + ], +}) +const taskLinkPolicy = pcmRoutes.defineReturnedLinkPolicy({ + endpoint: pcmTaskEndpoint, + relation: 'Job Status', + method: 'GET', + preserveGatewayBasePath: true, +}) + +/** The program-documentation details field is prose, so only a validated task link supplies an ID. */ +export function normalizeOraclePcmSubmission( + value: unknown, + client: OracleEpmClient +): OraclePcmTask { + const data = requireOraclePcmResponse( + pcmStatusSchema.extend({ + links: z + .array( + z.object({ + rel: z.string().max(255).optional(), + href: z.string().max(8_192), + action: z.string().max(16), + }) + ) + .max(32) + .optional(), + }), + value + ) + const links = (data.links ?? []).filter((link) => link.rel === 'Job Status') + if (links.length > 1 || (data.status === -1 && links.length !== 1)) { + throw new OraclePcmOperationError( + 'Oracle PCM omitted a unique task status link; inspect the Job Library before resubmitting', + 502 + ) + } + let processName: string | null = null + if (links.length === 1) { + const link = links[0] + client.validateReturnedLink(taskLinkPolicy, { + rel: 'Job Status', + href: link.href, + method: link.action, + }) + processName = decodeURIComponent(new URL(link.href).pathname.split('/').at(-1) ?? '') + requireOraclePcmResponse(pcmName, processName) + } + return normalizeOraclePcmTask(data, processName) +} +export const pcmAuthSchema = z.object({ + oauthCredential: pcmName, + accessToken: z.string().min(1).max(4_096), + instanceUrl: z.string().min(1).max(4_096), +}) +const applicationSchema = pcmAuthSchema.extend({ applicationName: pcmName }) +const optionalText = z.preprocess( + (value) => (value === '' || value === null ? undefined : value), + z.string().max(4_096).optional() +) +const optionalName = z.preprocess( + (value) => (value === '' || value === null ? undefined : value), + pcmName.optional() +) +const boolean = z.preprocess(parseOraclePcmBoolean, z.boolean().optional()) +const requiredBoolean = z.preprocess(parseOraclePcmBoolean, z.boolean()) +export const pcmNumber = (minimum: number, maximum: number, fallback?: number) => + z.preprocess( + (value) => + value === '' || value === undefined || value === null + ? fallback + : typeof value === 'string' && /^\d+(?:\.\d+)?$/.test(value) + ? Number(value) + : value, + z.number().finite().min(minimum).max(maximum).optional() + ) +const sequence = pcmNumber(0, 2_147_483_647) +const delimiter = z.preprocess( + (value) => (value === '' || value === null ? undefined : value), + z.string().length(1).optional() +) + +export function parseOraclePcmInput(schema: z.ZodType, value: unknown): T { + const values = isPlainRecord(value) + ? filterUndefined( + Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, item === '' ? undefined : item]) + ) + ) + : value + const parsed = schema.safeParse(values) + if (!parsed.success) { + const issue = parsed.error.issues[0] + throw new OraclePcmOperationError( + `Invalid PCM input ${issue?.path.join('.') || 'parameters'}: ${issue?.message || 'validation failed'}` + ) + } + return parsed.data +} + +export function oraclePcmClient(input: unknown): OracleEpmClient { + return createOracleEpmClient(parseOraclePcmInput(pcmAuthSchema, input)) +} + +export async function requestOraclePcmJson( + client: OracleEpmClient, + endpoint: OracleEpmEndpoint, + input: Parameters[1] +): Promise { + const response = await client.request(endpoint, input) + if (!('data' in response)) + throw new OraclePcmOperationError('Oracle PCM returned an unexpected response type', 502) + return response.data +} + +const appPath = [literal('applications'), parameter('applicationName', { maxBytes: 255 })] +const fileAppPath = [literal('fileApplications'), parameter('applicationName', { maxBytes: 255 })] +const povPath = [...appPath, literal('povs'), parameter('povName', { maxBytes: 255 })] +const post = (path: Parameters[0]['path']) => + pcmRoutes.defineEndpoint({ + ...pcmJsonPolicy, + method: 'POST', + path, + body: 'json', + maxRequestBytes: PCM_MAX_JSON_BYTES, + }) + +/** Product contracts from Oracle's PCM REST chapter; these are not EPCM job types. */ +const endpoints = { + create_application: post(fileAppPath), + enable_application: post([...fileAppPath, literal('enableApplication')]), + deploy_cube: post([...appPath, literal('jobs'), literal('ledgerDeployCubeJob')]), + update_dimensions: post([...fileAppPath, literal('jobs'), literal('updateDimension')]), + load_data: post([...appPath, literal('jobs'), literal('essbaseDataLoadJob')]), + run_calculation: post([...povPath, literal('jobs'), literal('runLedgerCalculationJob')]), + copy_pov: post([ + ...povPath, + literal('jobs'), + literal('copyPOVJob'), + parameter('destinationPovName', { maxBytes: 255 }), + ]), + clear_pov: post([...povPath, literal('jobs'), literal('clearPOVJob')]), + generate_program_documentation: post([ + ...povPath, + literal('jobs'), + literal('programDocReportJob'), + ]), + export_query_results: post([...appPath, literal('jobs'), literal('exportQueryResultsJob')]), + import_template: post([...appPath, literal('jobs'), literal('templateImportJob')]), + apply_data_grants: post([...appPath, literal('jobs'), literal('applyDataGrants')]), + merge_slices: post([...appPath, literal('jobs'), literal('mergeSlices')]), + optimize_cube: post([...appPath, literal('jobs'), literal('optimizeASOCube')]), +} +const balanceEndpoint = pcmRoutes.defineEndpoint({ + ...pcmJsonPolicy, + method: 'GET', + path: [...povPath, literal('ruleBalance')], + body: 'none', + query: { queryParameter: oracleEpmQuery.string({ required: true, maxBytes: 4_096 }) }, +}) +const povSchema = applicationSchema.extend({ povName: pcmName, stringDelimiter: delimiter }) +const schemas = { + create_application: applicationSchema.extend({ + description: z.string().max(4_096), + ruleDimensionName: pcmName, + balanceDimensionName: pcmName, + }), + enable_application: applicationSchema, + deploy_cube: applicationSchema + .extend({ + isKeepData: requiredBoolean, + isReplaceCube: requiredBoolean, + comment: z.string().max(4_096), + }) + .refine( + (p) => !(p.isKeepData && p.isReplaceCube), + 'Preserving data and replacing the cube cannot both be true' + ), + update_dimensions: applicationSchema.extend({ + dataFileName: z.string().trim().min(1).max(4_096), + stringDelimiter: delimiter, + acceptableDecreasePercentage: pcmNumber(0, 100), + }), + load_data: applicationSchema.extend({ + clearAllDataFlag: requiredBoolean, + dataLoadValue: z.enum(['ADD_EXISTING_VALUES', 'OVERWRITE_EXISTING_VALUES']), + dataFileName: pcmFileName, + }), + run_calculation: povSchema + .extend({ + exeType: z.enum(['ALL_RULES', 'RULESET_SUBSET', 'SINGLE_RULE']), + dataPOVName: optionalName, + isClearCalculated: boolean, + optimizeReporting: boolean, + subsetStart: sequence, + subsetEnd: sequence, + ruleName: optionalName, + ruleSetName: optionalName, + comment: optionalText, + }) + .superRefine((p, ctx) => { + if (p.dataPOVName && p.exeType !== 'ALL_RULES') { + ctx.addIssue({ + code: 'custom', + path: ['dataPOVName'], + message: 'A different data POV requires ALL_RULES', + }) + } + if (p.exeType === 'SINGLE_RULE' && (!p.ruleName || !p.ruleSetName)) { + ctx.addIssue({ + code: 'custom', + path: ['ruleName'], + message: 'SINGLE_RULE requires ruleName and ruleSetName', + }) + } + if ( + p.exeType === 'RULESET_SUBSET' && + (p.subsetStart === undefined || p.subsetEnd === undefined || p.subsetStart > p.subsetEnd) + ) { + ctx.addIssue({ + code: 'custom', + path: ['subsetStart'], + message: 'RULESET_SUBSET requires an ordered start/end range', + }) + } + }), + copy_pov: povSchema.extend({ + destinationPovName: pcmName, + isManageRule: requiredBoolean, + isInputData: requiredBoolean, + createDestPOV: requiredBoolean, + modelViewName: optionalName, + nonEmptyTupleEnabled: boolean, + stringDelimiter: z.string().length(1), + }), + clear_pov: povSchema + .extend({ + isManageRule: boolean, + isInputData: boolean, + isAllocatedValues: boolean, + isAdjustmentValues: boolean, + queryName: optionalName, + }) + .refine( + (p) => !p.queryName || !(p.isManageRule || p.isAllocatedValues || p.isAdjustmentValues), + 'Query clearing cannot clear rules, allocations, or adjustments' + ), + generate_program_documentation: povSchema.extend({ + fileName: pcmFileName.optional(), + fileType: z.enum(['PDF', 'XML', 'WORD', 'EXCEL', 'HTML']).optional(), + skipFilters: boolean, + subsetStart: sequence, + subsetEnd: sequence, + useAlias: boolean, + }), + export_query_results: applicationSchema.extend({ + fileName: pcmFileName, + queryName: optionalName, + exportOnlyLevel0Flg: boolean, + fileOutputOptions: z.enum(['ZIP_ONLY', 'ZIP_AND_TEXT', 'TEXT_ONLY']).optional(), + roundingPrecision: pcmNumber(0, Number.MAX_SAFE_INTEGER).pipe(z.number().int().optional()), + dataFormat: z.enum(['NATIVE', 'COLUMNAR']).optional(), + memberFilters: z.string().max(65_536).optional(), + includeHeader: boolean, + delimiter, + keepDuplicateMemberFormat: boolean, + }), + import_template: applicationSchema.extend({ + description: z.string().max(4_096), + fileName: pcmFileName, + isApplicationOverwrite: requiredBoolean, + }), + apply_data_grants: applicationSchema, + merge_slices: applicationSchema.extend({ removeZeroCells: boolean }), + optimize_cube: applicationSchema.extend({ + type: z.enum([ + 'clearAggregations', + 'createAggregations', + 'startQueryTracking', + 'stopQueryTracking', + 'createQBOAggregations', + ]), + }), +} + +export async function executeOraclePcmOperation( + operation: string, + input: unknown, + signal?: AbortSignal +): Promise { + signal?.throwIfAborted() + if (operation === 'get_rule_balancing') { + const p = parseOraclePcmInput(povSchema.extend({ modelViewName: pcmName }), input) + const data = requireOraclePcmResponse( + pcmStatusSchema.extend({ items: z.array(pcmBalanceSchema).max(PCM_MAX_ITEMS).optional() }), + await requestOraclePcmJson(oraclePcmClient(p), balanceEndpoint, { + pathParams: { applicationName: p.applicationName, povName: p.povName }, + query: { + queryParameter: JSON.stringify( + filterUndefined({ modelViewName: p.modelViewName, stringDelimiter: p.stringDelimiter }) + ), + }, + signal, + }) + ) + if (data.status !== 0) + throw new OraclePcmOperationError('Oracle PCM rule balancing failed', 502) + if (!data.items) + throw new OraclePcmOperationError('Oracle PCM omitted rule balancing items', 502) + return { success: true, output: { items: data.items, status: data.status } } + } + if (!Object.hasOwn(schemas, operation)) { + throw new OraclePcmOperationError('Unsupported Oracle PCM operation') + } + const key = operation as keyof typeof schemas + const p = parseOraclePcmInput(schemas[key] as z.ZodType>, input) + const pathParams: Record = { applicationName: p.applicationName as string } + if (typeof p.povName === 'string') pathParams.povName = p.povName + if (typeof p.destinationPovName === 'string') pathParams.destinationPovName = p.destinationPovName + let body = omit(p, [ + 'oauthCredential', + 'accessToken', + 'instanceUrl', + 'applicationName', + 'povName', + 'destinationPovName', + ]) + if (key === 'run_calculation' || key === 'deploy_cube') body.isRunNow = true + if (key === 'run_calculation') { + body.isExecuteCalculations = true + if (body.exeType !== 'RULESET_SUBSET') body = omit(body, ['subsetStart', 'subsetEnd']) + if (body.exeType !== 'SINGLE_RULE') body = omit(body, ['ruleName', 'ruleSetName']) + } + if (key === 'export_query_results' && typeof body.memberFilters === 'string') { + let filters: unknown + try { + filters = JSON.parse(body.memberFilters) + } catch { + throw new OraclePcmOperationError( + 'memberFilters must be a JSON object of dimension names to level-0 member arrays' + ) + } + parseOraclePcmInput( + z + .record(pcmName, z.array(pcmName).min(1).max(1_000)) + .refine((v) => Object.keys(v).length <= 100), + filters + ) + } + /** Match documented PCM string scalar payloads; report examples use native booleans. */ + const json = Object.fromEntries( + Object.entries(filterUndefined(body)).map(([name, value]) => [ + name, + (typeof value === 'boolean' && key !== 'generate_program_documentation') || + typeof value === 'number' + ? String(value) + : value, + ]) + ) + const client = oraclePcmClient(p) + const output = normalizeOraclePcmSubmission( + await requestOraclePcmJson(client, endpoints[key], { pathParams, json, signal }), + client + ) + return { + success: output.state !== 'failed', + output, + retryable: false, + ...(output.state === 'failed' + ? { error: 'Oracle PCM rejected the task; inspect its status and details before retrying' } + : {}), + } +} diff --git a/apps/sim/lib/internal/tool-operations/registry.server.ts b/apps/sim/lib/internal/tool-operations/registry.server.ts index 45bb6cb5cd4..c32d9d97845 100644 --- a/apps/sim/lib/internal/tool-operations/registry.server.ts +++ b/apps/sim/lib/internal/tool-operations/registry.server.ts @@ -783,6 +783,29 @@ const NETSUITE_TOOL_IDS = [ ] as const const OKTA_TOOL_IDS = ['okta_update_group'] as const + +const ORACLE_PCM_TOOL_IDS = [ + 'oracle_epm_profitability_apply_data_grants', + 'oracle_epm_profitability_clear_pov', + 'oracle_epm_profitability_copy_pov', + 'oracle_epm_profitability_create_application', + 'oracle_epm_profitability_deploy_cube', + 'oracle_epm_profitability_download_file', + 'oracle_epm_profitability_enable_application', + 'oracle_epm_profitability_export_query_results', + 'oracle_epm_profitability_generate_program_documentation', + 'oracle_epm_profitability_get_rule_balancing', + 'oracle_epm_profitability_get_task_status', + 'oracle_epm_profitability_import_template', + 'oracle_epm_profitability_list_files', + 'oracle_epm_profitability_load_data', + 'oracle_epm_profitability_merge_slices', + 'oracle_epm_profitability_optimize_cube', + 'oracle_epm_profitability_run_calculation', + 'oracle_epm_profitability_update_dimensions', + 'oracle_epm_profitability_upload_file', + 'oracle_epm_profitability_wait_for_task', +] as const const SALESFORCE_TOOL_IDS = ['salesforce_update_custom_field'] as const const SLACK_TOOL_IDS = [ @@ -1431,6 +1454,9 @@ registerFamily(handlerLoaders, NETSUITE_TOOL_IDS, async () => { registerFamily(handlerLoaders, OKTA_TOOL_IDS, async () => { return (await import('@/lib/internal/okta/execute-tool')).executeOktaTool }) +registerFamily(handlerLoaders, ORACLE_PCM_TOOL_IDS, async () => { + return (await import('@/lib/internal/oracle-epm-profitability/execute-tool')).executeOraclePcmTool +}) registerFamily(handlerLoaders, SALESFORCE_TOOL_IDS, async () => { return (await import('@/lib/internal/salesforce/execute-tool')).executeSalesforceTool }) diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 692a59510fe..53f2ca40260 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -1028,6 +1028,24 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'netsuite', }, + 'oracle-epm-profitability': { + name: 'Oracle EPM Profitability and Cost Management (PCM)', + icon: NetSuiteIcon, + services: { + 'oracle-epm-profitability': { + name: 'Oracle PCM', + description: + 'Operate PCM applications, allocations, POVs, reports, tasks, and PCM repository files.', + providerId: 'oracle-epm-profitability', + serviceAccountProviderId: 'oracle-epm-service-account', + icon: NetSuiteIcon, + baseProviderIcon: NetSuiteIcon, + scopes: [], + authType: 'service_account', + }, + }, + defaultService: 'oracle-epm-profitability', + }, reddit: { name: 'Reddit', icon: RedditIcon, diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index d7b5b668f2a..58aeb17cd63 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -115,6 +115,7 @@ export type OAuthService = | 'airtable' | 'bitbucket' | 'notion' + | 'oracle-epm-profitability' | 'jira' | 'atlassian-service-account' | 'box' diff --git a/apps/sim/lib/selectors/manifest.test.ts b/apps/sim/lib/selectors/manifest.test.ts index df9322d33e3..c26c254dfb9 100644 --- a/apps/sim/lib/selectors/manifest.test.ts +++ b/apps/sim/lib/selectors/manifest.test.ts @@ -9,8 +9,8 @@ describe('selector manifest', () => { const count = (classification: (typeof classifications)[number]) => classifications.filter((value) => value === classification).length - expect(Object.keys(selectorManifest)).toHaveLength(94) - expect(count('provider-server')).toBe(82) + expect(Object.keys(selectorManifest)).toHaveLength(96) + expect(count('provider-server')).toBe(84) expect(count('internal-server')).toBe(11) expect(count('local')).toBe(1) expect(classifications).not.toContain('provider-legacy') @@ -36,7 +36,7 @@ describe('selector manifest', () => { const rawConnectionKeys = providerKeys.filter( (key) => !serverSelectorRegistry[key as keyof typeof serverSelectorRegistry].credential ) - expect(providerKeys).toHaveLength(82) + expect(providerKeys).toHaveLength(84) expect(rawConnectionKeys.sort()).toEqual([ 'cloudwatch.logGroups', 'cloudwatch.logStreams', @@ -44,6 +44,22 @@ describe('selector manifest', () => { ]) }) + it.each(['oracleEpmPcm.inputFiles', 'oracleEpmPcm.outputFiles'] as const)( + 'binds %s to PCM credentials and list-backed detail resolution', + (key) => { + expect(selectorManifest[key].context.readiness).toEqual({ all: ['oauthCredential'] }) + expect(serverSelectorRegistry[key].credential).toMatchObject({ + kind: 'stored', + field: 'oauthCredential', + serviceIds: ['oracle-epm-profitability'], + }) + expect(serverSelectorRegistry[key].integrationBlockTypes).toEqual([ + 'oracle_epm_profitability', + ]) + expect(serverSelectorRegistry[key].destination).toMatchObject({ kind: 'credential-bound' }) + } + ) + it('keeps shared Microsoft selectors bound only to their intended credential families', () => { expect(serverSelectorRegistry['onedrive.files'].credential?.serviceIds).toEqual(['onedrive']) expect(serverSelectorRegistry['onedrive.folders'].credential?.serviceIds).toEqual([ @@ -98,7 +114,7 @@ describe('selector manifest', () => { (attachment) => attachment.destination !== 'fixed' ) - expect(preparedDestinations).toHaveLength(13) + expect(preparedDestinations).toHaveLength(15) for (const attachment of preparedDestinations) { expect(attachment.destination).toEqual( expect.objectContaining({ diff --git a/apps/sim/lib/selectors/manifest.ts b/apps/sim/lib/selectors/manifest.ts index 172fcfa0ac5..312fc9553eb 100644 --- a/apps/sim/lib/selectors/manifest.ts +++ b/apps/sim/lib/selectors/manifest.ts @@ -188,6 +188,8 @@ export const selectorManifest = { detail: true, unknownDetail: true, }), + 'oracleEpmPcm.inputFiles': providerSelector([], { detail: true }), + 'oracleEpmPcm.outputFiles': providerSelector([], { detail: true }), 'pipedrive.pipelines': providerSelector([], { detail: true }), 'sharepoint.lists': providerSelector(['siteId'], { readiness: { all: ['oauthCredential', 'siteId'] }, diff --git a/apps/sim/lib/selectors/server/providers/oracle-epm-profitability.test.ts b/apps/sim/lib/selectors/server/providers/oracle-epm-profitability.test.ts new file mode 100644 index 00000000000..d497bbf8227 --- /dev/null +++ b/apps/sim/lib/selectors/server/providers/oracle-epm-profitability.test.ts @@ -0,0 +1,155 @@ +/** @vitest-environment node */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ files: vi.fn(), credential: vi.fn(), bundle: vi.fn() })) +vi.mock('@/lib/internal/oracle-epm-profitability/files.server', () => ({ + listOraclePcmFiles: mocks.files, +})) +vi.mock('@/lib/oauth/credential-service', () => ({ resolveOAuthAccountId: mocks.credential })) +vi.mock('@/lib/selectors/server/providers/credential-bundle', () => ({ + resolveSelectorCredentialBundle: mocks.bundle, +})) +vi.mock('@/lib/internal/oracle-epm/files.server', () => ({ + openOracleEpmSourceFile: vi.fn(), + storeOracleEpmDownload: vi.fn(), +})) + +import { oracleEpmLocalError } from '@/lib/internal/oracle-epm/errors' +import { isSelectorReady } from '@/lib/selectors/manifest' +import { createSelectorProtectedValues } from '@/lib/selectors/server/protected-values' +import { oraclePcmSelectorAttachments } from '@/lib/selectors/server/providers/oracle-epm-profitability' +import type { ExecuteServerSelectorArgs } from '@/lib/selectors/server/types' + +const auth = { + oauthCredential: 'credential-1', + accessToken: 'server-token', + instanceUrl: 'https://epm.example.com/gateway', +} +function args( + key: keyof typeof oraclePcmSelectorAttachments = 'oracleEpmPcm.inputFiles' +): ExecuteServerSelectorArgs { + return { + selectorKey: key, + context: { oauthCredential: 'credential-1' }, + request: { kind: 'list' }, + scope: { kind: 'workspace', workspaceId: 'workspace-1' }, + workspaceId: 'workspace-1', + principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, + requesterUserId: 'user-1', + credential: { + suppliedId: 'credential-1', + access: { + ok: true, + resolvedCredentialId: 'credential-1', + credentialType: 'service_account', + credentialOwnerUserId: 'user-1', + }, + }, + references: new Map(), + protectedValues: createSelectorProtectedValues(), + signal: new AbortController().signal, + } +} + +describe('Oracle PCM repository selectors', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.credential.mockResolvedValue({ + credentialType: 'service_account', + providerId: 'oracle-epm-service-account', + }) + mocks.bundle.mockResolvedValue({ accessToken: auth.accessToken, instanceUrl: auth.instanceUrl }) + mocks.files.mockResolvedValue([ + { name: 'profitinbox/data.csv', size: 123, untrusted: 'not projected' }, + { name: 'profitinbox/nested/data.csv', size: 123 }, + { name: 'profitoutbox/report.csv ', size: null }, + { name: 'profitoutbox/report.csv ', size: null }, + { name: `profitoutbox/${'é'.repeat(123)}.csv`, size: 123 }, + ]) + }) + + it('binds input files to credential authority and returns usable bare filenames', async () => { + const request = args() + expect( + await oraclePcmSelectorAttachments[request.selectorKey as 'oracleEpmPcm.inputFiles'].execute( + request + ) + ).toEqual({ + kind: 'list', + items: [{ id: 'data.csv', label: 'data.csv', meta: { size: 123 } }], + }) + expect(mocks.credential).toHaveBeenCalledWith('credential-1') + expect(mocks.files).toHaveBeenCalledWith(auth, request.signal) + expect(mocks.bundle).toHaveBeenCalledWith({ + credential: request.credential, + protectedValues: request.protectedValues, + }) + }) + + it('returns exact outbox paths without links, duplicates, or unusable long names', async () => { + expect( + await oraclePcmSelectorAttachments['oracleEpmPcm.outputFiles'].execute( + args('oracleEpmPcm.outputFiles') + ) + ).toEqual({ + kind: 'list', + items: [{ id: 'profitoutbox/report.csv ', label: 'report.csv ', meta: { size: null } }], + }) + }) + + it.each(['netsuite-service-account', 'google-drive', undefined])( + 'rejects non-EPM provider %s before credential minting or remote discovery', + async (providerId) => { + mocks.credential.mockResolvedValue({ credentialType: 'service_account', providerId }) + await expect( + oraclePcmSelectorAttachments['oracleEpmPcm.inputFiles'].execute(args()) + ).rejects.toThrow() + expect(mocks.bundle).not.toHaveBeenCalled() + expect(mocks.files).not.toHaveBeenCalled() + } + ) + + it('requires authorized credentials and a credential-owned destination', async () => { + const attachment = oraclePcmSelectorAttachments['oracleEpmPcm.inputFiles'] + await expect(attachment.execute({ ...args(), credential: undefined })).rejects.toThrow() + expect(mocks.bundle).not.toHaveBeenCalled() + mocks.bundle.mockResolvedValue({ accessToken: 'server-token' }) + await expect(attachment.execute(args())).rejects.toThrow() + expect(mocks.files).not.toHaveBeenCalled() + }) + + it('resolves details only from the same filtered list', async () => { + const attachment = oraclePcmSelectorAttachments['oracleEpmPcm.inputFiles'] + expect( + await attachment.execute({ ...args(), request: { kind: 'detail', id: 'data.csv' } }) + ).toEqual({ + kind: 'detail', + item: { id: 'data.csv', label: 'data.csv', meta: { size: 123 } }, + }) + expect( + await attachment.execute({ ...args(), request: { kind: 'detail', id: 'report.csv' } }) + ).toEqual({ + kind: 'detail', + item: null, + }) + mocks.files.mockResolvedValue([]) + expect(await attachment.execute(args())).toEqual({ kind: 'list', items: [] }) + }) + + it('preserves safe failures and cancellation', async () => { + const attachment = oraclePcmSelectorAttachments['oracleEpmPcm.inputFiles'] + mocks.files.mockRejectedValue(oracleEpmLocalError('invalid_response')) + await expect(attachment.execute(args())).rejects.toThrow() + mocks.files.mockClear() + await expect(attachment.execute({ ...args(), signal: AbortSignal.abort() })).rejects.toThrow() + expect(mocks.files).not.toHaveBeenCalled() + }) + + it.each(['oracleEpmPcm.inputFiles', 'oracleEpmPcm.outputFiles'] as const)( + '%s requires only the shared credential, without speculative discovery inputs', + (key) => { + expect(isSelectorReady(key, {})).toBe(false) + expect(isSelectorReady(key, { oauthCredential: 'credential-1' })).toBe(true) + } + ) +}) diff --git a/apps/sim/lib/selectors/server/providers/oracle-epm-profitability.ts b/apps/sim/lib/selectors/server/providers/oracle-epm-profitability.ts new file mode 100644 index 00000000000..46374b72c0c --- /dev/null +++ b/apps/sim/lib/selectors/server/providers/oracle-epm-profitability.ts @@ -0,0 +1,115 @@ +import { ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID } from '@/lib/credentials/client-credential-accounts/descriptors' +import { normalizeOracleEpmDestination, OracleEpmError } from '@/lib/internal/oracle-epm' +import { listOraclePcmFiles } from '@/lib/internal/oracle-epm-profitability/files.server' +import { + isOraclePcmDownloadablePath, + OraclePcmOperationError, +} from '@/lib/internal/oracle-epm-profitability/normalizers' +import { resolveOAuthAccountId } from '@/lib/oauth/credential-service' +import type { ServerSelectorKey } from '@/lib/selectors/manifest' +import { + SelectorConnectionUnavailableError, + SelectorOptionsUnavailableError, +} from '@/lib/selectors/server/errors' +import { resolveSelectorCredentialBundle } from '@/lib/selectors/server/providers/credential-bundle' +import { flatSelectorResult } from '@/lib/selectors/server/providers/flat-results' +import { selectorProviderStatusError } from '@/lib/selectors/server/providers/provider-http' +import { + definePreparedSelectorAttachment, + type ExecuteServerSelectorArgs, + type ServerSelectorAttachmentMap, +} from '@/lib/selectors/server/types' +import type { SafeSelectorOption } from '@/lib/selectors/types' +import type { OraclePcmAuthParams } from '@/tools/oracle_epm_profitability/types' + +type OraclePcmSelectorKey = Extract< + ServerSelectorKey, + 'oracleEpmPcm.inputFiles' | 'oracleEpmPcm.outputFiles' +> +type PreparedOraclePcmDestination = OraclePcmAuthParams & { + instanceUrl: string + accessToken: string +} + +async function prepareDestination( + args: ExecuteServerSelectorArgs +): Promise { + args.signal?.throwIfAborted() + const access = args.credential?.access + if (access?.credentialType !== 'service_account' || !access.resolvedCredentialId) { + throw new SelectorConnectionUnavailableError() + } + const resolved = await resolveOAuthAccountId(access.resolvedCredentialId) + args.signal?.throwIfAborted() + if ( + resolved?.credentialType !== 'service_account' || + resolved.providerId !== ORACLE_EPM_SERVICE_ACCOUNT_PROVIDER_ID + ) { + throw new SelectorConnectionUnavailableError() + } + const token = await resolveSelectorCredentialBundle({ + credential: args.credential, + protectedValues: args.protectedValues, + }) + if (!token.instanceUrl) throw new SelectorConnectionUnavailableError() + let instanceUrl: string + try { + instanceUrl = normalizeOracleEpmDestination(token.instanceUrl) + } catch { + throw new SelectorConnectionUnavailableError() + } + return { + oauthCredential: access.resolvedCredentialId, + accessToken: token.accessToken, + instanceUrl, + } +} + +async function executeSelector( + args: ExecuteServerSelectorArgs, + auth: PreparedOraclePcmDestination +) { + args.signal?.throwIfAborted() + let options: SafeSelectorOption[] + try { + const inputFiles = args.selectorKey === 'oracleEpmPcm.inputFiles' + const prefix = inputFiles ? 'profitinbox/' : 'profitoutbox/' + options = (await listOraclePcmFiles(auth, args.signal)) + .filter((file) => isOraclePcmDownloadablePath(file.name)) + .filter( + (file) => + file.name.startsWith(prefix) && + (!inputFiles || !file.name.slice(prefix.length).includes('/')) + ) + .map((file) => ({ + id: inputFiles ? file.name.slice(prefix.length) : file.name, + label: file.name.slice(prefix.length), + meta: { size: file.size }, + })) + } catch (error) { + args.signal?.throwIfAborted() + if (error instanceof OracleEpmError) throw selectorProviderStatusError(error.status ?? 502) + if (error instanceof OraclePcmOperationError) throw new SelectorOptionsUnavailableError() + throw error + } + const unique = [...new Map(options.map((option) => [option.id, option])).values()].sort( + (left, right) => left.label.localeCompare(right.label) + ) + return flatSelectorResult(args.request, unique, true) +} + +const attachment = { + credential: { + kind: 'stored', + field: 'oauthCredential', + serviceIds: ['oracle-epm-profitability'], + }, + integrationBlockTypes: ['oracle_epm_profitability'], + destination: { kind: 'credential-bound', prepare: prepareDestination }, + execute: executeSelector, +} as const + +export const oraclePcmSelectorAttachments = { + 'oracleEpmPcm.inputFiles': definePreparedSelectorAttachment(attachment), + 'oracleEpmPcm.outputFiles': definePreparedSelectorAttachment(attachment), +} satisfies ServerSelectorAttachmentMap diff --git a/apps/sim/lib/selectors/server/registry.ts b/apps/sim/lib/selectors/server/registry.ts index ef9a815f1af..b3aa6193b6b 100644 --- a/apps/sim/lib/selectors/server/registry.ts +++ b/apps/sim/lib/selectors/server/registry.ts @@ -21,6 +21,7 @@ import { microsoftSelectorAttachments } from '@/lib/selectors/server/providers/m import { mondaySelectorAttachments } from '@/lib/selectors/server/providers/monday' import { netsuiteSelectorAttachments } from '@/lib/selectors/server/providers/netsuite' import { notionSelectorAttachments } from '@/lib/selectors/server/providers/notion' +import { oraclePcmSelectorAttachments } from '@/lib/selectors/server/providers/oracle-epm-profitability' import { pipedriveSelectorAttachments } from '@/lib/selectors/server/providers/pipedrive' import { sharepointSelectorAttachments } from '@/lib/selectors/server/providers/sharepoint' import { slackSelectorAttachments } from '@/lib/selectors/server/providers/slack' @@ -55,6 +56,7 @@ export const serverSelectorRegistry = { ...mondaySelectorAttachments, ...netsuiteSelectorAttachments, ...notionSelectorAttachments, + ...oraclePcmSelectorAttachments, ...pipedriveSelectorAttachments, ...sharepointSelectorAttachments, ...slackSelectorAttachments, diff --git a/apps/sim/tools/generated/tool-ids.ts b/apps/sim/tools/generated/tool-ids.ts index 6d97df92a01..4a5c2900c87 100644 --- a/apps/sim/tools/generated/tool-ids.ts +++ b/apps/sim/tools/generated/tool-ids.ts @@ -3,7 +3,7 @@ /** Every registered tool id, including versioned variants. */ const toolIds: string[] = JSON.parse( - '["a2a_cancel_task","a2a_get_agent_card","a2a_get_task","a2a_send_message","affinity_batch_update_entity_fields","affinity_batch_update_list_entry_fields","affinity_create_list","affinity_create_list_field_dropdown_option","affinity_create_merge","affinity_create_note","affinity_create_reminder","affinity_delete_list_field_dropdown_option","affinity_delete_note","affinity_get_company","affinity_get_current_user","affinity_get_entity_field_value","affinity_get_list","affinity_get_list_entry","affinity_get_list_entry_field","affinity_get_list_field_dropdown_option","affinity_get_merge","affinity_get_merge_task","affinity_get_note","affinity_get_opportunity","affinity_get_person","affinity_get_saved_view","affinity_get_transcript","affinity_get_user","affinity_list_calls","affinity_list_chat_messages","affinity_list_companies","affinity_list_coworker_connections","affinity_list_emails","affinity_list_entity_field_values","affinity_list_entity_list_entries","affinity_list_entity_lists","affinity_list_entity_notes","affinity_list_entity_relationships","affinity_list_field_dropdown_options","affinity_list_field_metadata","affinity_list_field_value_changes","affinity_list_investor_executive_connections","affinity_list_list_entries","affinity_list_list_entry_field_value_changes","affinity_list_list_entry_fields","affinity_list_list_field_dropdown_options","affinity_list_list_fields","affinity_list_lists","affinity_list_meetings","affinity_list_merge_tasks","affinity_list_merges","affinity_list_note_attached_companies","affinity_list_note_attached_opportunities","affinity_list_note_attached_persons","affinity_list_note_replies","affinity_list_notes","affinity_list_opportunities","affinity_list_persons","affinity_list_reminders","affinity_list_saved_view_entries","affinity_list_saved_views","affinity_list_transcript_fragments","affinity_list_transcripts","affinity_list_users","affinity_search_companies","affinity_search_files","affinity_search_list_entries","affinity_search_notes","affinity_search_persons","affinity_semantic_search","affinity_update_entity_field_value","affinity_update_list_entry_field","affinity_update_list_field_dropdown_option","affinity_update_note","agentmail_create_draft","agentmail_create_inbox","agentmail_delete_draft","agentmail_delete_inbox","agentmail_delete_thread","agentmail_forward_message","agentmail_get_draft","agentmail_get_inbox","agentmail_get_message","agentmail_get_thread","agentmail_list_drafts","agentmail_list_inboxes","agentmail_list_messages","agentmail_list_threads","agentmail_reply_message","agentmail_send_draft","agentmail_send_message","agentmail_update_draft","agentmail_update_inbox","agentmail_update_message","agentmail_update_thread","agentphone_create_call","agentphone_create_contact","agentphone_create_number","agentphone_delete_contact","agentphone_get_call","agentphone_get_call_transcript","agentphone_get_contact","agentphone_get_conversation","agentphone_get_conversation_messages","agentphone_get_number_messages","agentphone_get_usage","agentphone_get_usage_daily","agentphone_get_usage_monthly","agentphone_list_calls","agentphone_list_contacts","agentphone_list_conversations","agentphone_list_numbers","agentphone_react_to_message","agentphone_release_number","agentphone_send_message","agentphone_update_contact","agentphone_update_conversation","agiloft_async_status","agiloft_attach_file","agiloft_attachment_info","agiloft_create_record","agiloft_delete_record","agiloft_get_choice_line_id","agiloft_list_tables","agiloft_lock_record","agiloft_nlp_search","agiloft_read_record","agiloft_remove_attachment","agiloft_retrieve_attachment","agiloft_run_action_button","agiloft_saved_search","agiloft_search_records","agiloft_select_records","agiloft_update_record","agiloft_upsert_record","ahrefs_anchors","ahrefs_backlinks","ahrefs_backlinks_stats","ahrefs_batch_analysis","ahrefs_broken_backlinks","ahrefs_domain_rating","ahrefs_domain_rating_history","ahrefs_keyword_overview","ahrefs_keywords_history","ahrefs_metrics","ahrefs_metrics_history","ahrefs_organic_competitors","ahrefs_organic_keywords","ahrefs_paid_pages","ahrefs_rank_tracker_competitors_overview","ahrefs_rank_tracker_competitors_stats","ahrefs_rank_tracker_overview","ahrefs_rank_tracker_serp_overview","ahrefs_refdomains_history","ahrefs_referring_domains","ahrefs_related_terms","ahrefs_site_audit_page_explorer","ahrefs_top_pages","airtable_create_records","airtable_delete_records","airtable_get_base_schema","airtable_get_record","airtable_list_bases","airtable_list_records","airtable_list_tables","airtable_update_multiple_records","airtable_update_record","airtable_upsert_records","airweave_search","algolia_add_record","algolia_batch_operations","algolia_browse_records","algolia_clear_records","algolia_copy_move_index","algolia_delete_by_filter","algolia_delete_index","algolia_delete_record","algolia_get_record","algolia_get_records","algolia_get_settings","algolia_get_task_status","algolia_list_indices","algolia_partial_update_record","algolia_search","algolia_update_settings","amplitude_event_segmentation","amplitude_funnels","amplitude_get_active_users","amplitude_get_revenue","amplitude_group_identify","amplitude_identify_user","amplitude_list_events","amplitude_realtime_active_users","amplitude_retention","amplitude_send_event","amplitude_user_activity","amplitude_user_profile","amplitude_user_search","apify_get_dataset_items","apify_get_run","apify_run_actor_async","apify_run_actor_sync","apify_run_task","apollo_account_bulk_create","apollo_account_bulk_update","apollo_account_create","apollo_account_search","apollo_account_update","apollo_contact_bulk_create","apollo_contact_bulk_update","apollo_contact_create","apollo_contact_search","apollo_contact_update","apollo_email_accounts","apollo_opportunity_create","apollo_opportunity_get","apollo_opportunity_search","apollo_opportunity_update","apollo_organization_bulk_enrich","apollo_organization_enrich","apollo_organization_search","apollo_people_bulk_enrich","apollo_people_enrich","apollo_people_search","apollo_sequence_add_contacts","apollo_sequence_search","apollo_task_create","apollo_task_search","appconfig_create_application","appconfig_create_configuration_profile","appconfig_create_environment","appconfig_create_hosted_configuration_version","appconfig_delete_application","appconfig_delete_configuration_profile","appconfig_delete_environment","appconfig_delete_hosted_configuration_version","appconfig_get_application","appconfig_get_configuration","appconfig_get_configuration_profile","appconfig_get_deployment","appconfig_get_environment","appconfig_get_hosted_configuration_version","appconfig_list_applications","appconfig_list_configuration_profiles","appconfig_list_deployment_strategies","appconfig_list_deployments","appconfig_list_environments","appconfig_list_hosted_configuration_versions","appconfig_start_deployment","appconfig_stop_deployment","appconfig_update_application","appconfig_update_configuration_profile","appconfig_update_environment","arxiv_get_author_papers","arxiv_get_paper","arxiv_search","asana_add_comment","asana_add_followers","asana_create_project","asana_create_section","asana_create_subtask","asana_create_task","asana_delete_task","asana_get_project","asana_get_projects","asana_get_task","asana_list_sections","asana_list_workspaces","asana_search_tasks","asana_update_task","ashby_add_candidate_tag","ashby_anonymize_candidate","ashby_change_application_source","ashby_change_application_stage","ashby_create_application","ashby_create_candidate","ashby_create_note","ashby_delete_application","ashby_get_application","ashby_get_candidate","ashby_get_job","ashby_get_job_posting","ashby_get_offer","ashby_get_opening","ashby_list_application_feedback","ashby_list_application_history","ashby_list_applications","ashby_list_archive_reasons","ashby_list_candidate_tags","ashby_list_candidates","ashby_list_custom_fields","ashby_list_departments","ashby_list_interview_plans","ashby_list_interview_stages","ashby_list_interviews","ashby_list_job_postings","ashby_list_jobs","ashby_list_locations","ashby_list_notes","ashby_list_offers","ashby_list_openings","ashby_list_sources","ashby_list_users","ashby_remove_candidate_tag","ashby_search_candidates","ashby_search_jobs","ashby_search_openings","ashby_search_users","ashby_set_custom_field_value","ashby_set_custom_field_values","ashby_transfer_application","ashby_update_candidate","ashby_upload_candidate_file","ashby_upload_resume","athena_batch_get_query_execution","athena_create_named_query","athena_delete_named_query","athena_get_named_query","athena_get_query_execution","athena_get_query_results","athena_list_databases","athena_list_named_queries","athena_list_query_executions","athena_list_table_metadata","athena_start_query","athena_stop_query","attio_assert_record","attio_create_attribute","attio_create_comment","attio_create_list","attio_create_list_entry","attio_create_note","attio_create_object","attio_create_record","attio_create_task","attio_create_webhook","attio_delete_comment","attio_delete_list_entry","attio_delete_note","attio_delete_record","attio_delete_task","attio_delete_webhook","attio_get_attribute","attio_get_comment","attio_get_list","attio_get_list_entry","attio_get_member","attio_get_note","attio_get_object","attio_get_record","attio_get_task","attio_get_thread","attio_get_webhook","attio_list_attributes","attio_list_lists","attio_list_members","attio_list_notes","attio_list_objects","attio_list_records","attio_list_tasks","attio_list_threads","attio_list_webhooks","attio_query_list_entries","attio_search_records","attio_update_attribute","attio_update_list","attio_update_list_entry","attio_update_object","attio_update_record","attio_update_task","attio_update_webhook","azure_data_explorer_create_table","azure_data_explorer_drop_table","azure_data_explorer_ingest_from_query","azure_data_explorer_ingest_inline","azure_data_explorer_list_databases","azure_data_explorer_list_functions","azure_data_explorer_list_tables","azure_data_explorer_management","azure_data_explorer_query","azure_data_explorer_show_database_schema","azure_data_explorer_show_ingestion_failures","azure_data_explorer_show_operations","azure_data_explorer_show_table_details","azure_data_explorer_show_table_schema","azure_devops_add_comment","azure_devops_create_work_item","azure_devops_get_build_log","azure_devops_get_build_timeline","azure_devops_get_comments","azure_devops_get_pipeline","azure_devops_get_pipeline_run","azure_devops_get_work_item","azure_devops_get_work_items_batch","azure_devops_get_work_items_between_builds","azure_devops_list_build_logs","azure_devops_list_builds","azure_devops_list_pipeline_runs","azure_devops_list_pipelines","azure_devops_query_work_items","azure_devops_update_work_item","bitbucket_approve_pull_request","bitbucket_create_branch","bitbucket_create_pull_request","bitbucket_create_pull_request_comment","bitbucket_decline_pull_request","bitbucket_delete_branch","bitbucket_get_commit","bitbucket_get_file","bitbucket_get_file_metadata","bitbucket_get_pipeline","bitbucket_get_pipeline_step_log","bitbucket_get_pull_request","bitbucket_get_pull_request_diff","bitbucket_get_pull_request_diffstat","bitbucket_get_pull_request_merge_task_status","bitbucket_get_repository","bitbucket_list_branches","bitbucket_list_commits","bitbucket_list_directory","bitbucket_list_pipeline_steps","bitbucket_list_pipelines","bitbucket_list_pull_request_comments","bitbucket_list_pull_request_commit_statuses","bitbucket_list_pull_requests","bitbucket_list_repositories","bitbucket_list_workspaces","bitbucket_merge_pull_request","bitbucket_request_pull_request_changes","bitbucket_stop_pipeline","bitbucket_trigger_pipeline","box_copy_file","box_create_folder","box_delete_file","box_delete_folder","box_download_file","box_get_file_info","box_list_folder_items","box_search","box_sign_cancel_request","box_sign_create_request","box_sign_get_request","box_sign_list_requests","box_sign_resend_request","box_update_file","box_upload_file","brandfetch_get_brand","brandfetch_search","brex_archive_budget","brex_create_budget","brex_create_spend_limit","brex_create_transfer","brex_create_vendor","brex_get_budget","brex_get_cash_account","brex_get_company","brex_get_current_user","brex_get_expense","brex_get_spend_limit","brex_get_transfer","brex_get_user","brex_get_vendor","brex_list_budgets","brex_list_card_accounts","brex_list_card_statements","brex_list_card_transactions","brex_list_cards","brex_list_cash_accounts","brex_list_cash_statements","brex_list_cash_transactions","brex_list_departments","brex_list_expenses","brex_list_locations","brex_list_spend_limits","brex_list_titles","brex_list_transfers","brex_list_users","brex_list_vendors","brex_match_receipt","brex_update_expense","brex_update_vendor","brex_upload_receipt","brightdata_cancel_snapshot","brightdata_discover","brightdata_download_snapshot","brightdata_scrape_dataset","brightdata_scrape_url","brightdata_serp_search","brightdata_snapshot_status","brightdata_sync_scrape","browser_use_run_task","buffer_create_idea","buffer_create_post","buffer_delete_post","buffer_edit_post","buffer_get_account","buffer_get_channels","buffer_get_idea_groups","buffer_get_ideas","buffer_get_post","buffer_get_posts","calcom_cancel_booking","calcom_confirm_booking","calcom_create_booking","calcom_create_event_type","calcom_create_schedule","calcom_decline_booking","calcom_delete_event_type","calcom_delete_schedule","calcom_get_booking","calcom_get_default_schedule","calcom_get_event_type","calcom_get_schedule","calcom_get_slots","calcom_list_bookings","calcom_list_event_types","calcom_list_schedules","calcom_reschedule_booking","calcom_update_event_type","calcom_update_schedule","calendly_cancel_event","calendly_create_event_invitee","calendly_create_invitee_no_show","calendly_create_scheduling_link","calendly_create_webhook","calendly_delete_invitee_no_show","calendly_delete_webhook","calendly_get_current_user","calendly_get_event_invitee","calendly_get_event_type","calendly_get_scheduled_event","calendly_get_user","calendly_list_event_invitees","calendly_list_event_type_available_times","calendly_list_event_types","calendly_list_organization_memberships","calendly_list_routing_form_submissions","calendly_list_routing_forms","calendly_list_scheduled_events","calendly_list_user_availability_schedules","calendly_list_user_busy_times","calendly_list_webhooks","cbinsights_chat","cbinsights_get_commercial_maturity_history","cbinsights_get_exit_probability_history","cbinsights_get_mosaic_history","cbinsights_get_org_business_relationships","cbinsights_get_org_funding_window","cbinsights_get_org_fundings","cbinsights_get_org_investments","cbinsights_get_org_management_and_board","cbinsights_get_org_outlook","cbinsights_get_org_portfolio_exits","cbinsights_get_org_revenue","cbinsights_get_scouting_report","cbinsights_get_strategy_map","cbinsights_list_business_relationships","cbinsights_list_funding_window","cbinsights_list_fundings","cbinsights_list_investments","cbinsights_list_management_and_board","cbinsights_list_outlook","cbinsights_list_portfolio_exits","cbinsights_list_revenue","cbinsights_lookup_organizations","cbinsights_rag","cbinsights_search_firmographics","circleback_add_tag_to_meetings","circleback_create_tag","circleback_delete_action_item","circleback_delete_meeting","circleback_delete_tag","circleback_get_company","circleback_get_meeting","circleback_get_person","circleback_get_transcript","circleback_list_action_items","circleback_list_calendar_events","circleback_list_companies","circleback_list_meetings","circleback_list_people","circleback_list_tags","circleback_remove_tag_from_meetings","circleback_search_meetings","circleback_update_action_item","circleback_update_meeting","circleback_update_tag","clay_populate","clerk_add_organization_member","clerk_ban_user","clerk_create_actor_token","clerk_create_allowlist_identifier","clerk_create_blocklist_identifier","clerk_create_organization","clerk_create_organization_invitation","clerk_create_user","clerk_delete_allowlist_identifier","clerk_delete_blocklist_identifier","clerk_delete_organization","clerk_delete_user","clerk_get_jwt_template","clerk_get_organization","clerk_get_session","clerk_get_user","clerk_get_user_oauth_token","clerk_list_allowlist_identifiers","clerk_list_blocklist_identifiers","clerk_list_jwt_templates","clerk_list_organization_invitations","clerk_list_organization_memberships","clerk_list_organizations","clerk_list_sessions","clerk_list_users","clerk_lock_user","clerk_remove_organization_member","clerk_revoke_actor_token","clerk_revoke_session","clerk_unban_user","clerk_unlock_user","clerk_update_organization","clerk_update_organization_membership","clerk_update_user","clickhouse_count_rows","clickhouse_create_database","clickhouse_create_table","clickhouse_delete","clickhouse_describe_table","clickhouse_drop_database","clickhouse_drop_partition","clickhouse_drop_table","clickhouse_execute","clickhouse_insert","clickhouse_insert_rows","clickhouse_introspect","clickhouse_kill_query","clickhouse_list_clusters","clickhouse_list_databases","clickhouse_list_mutations","clickhouse_list_partitions","clickhouse_list_running_queries","clickhouse_list_tables","clickhouse_optimize_table","clickhouse_query","clickhouse_rename_table","clickhouse_show_create_table","clickhouse_table_stats","clickhouse_truncate_table","clickhouse_update","clickup_add_tag_to_task","clickup_create_checklist","clickup_create_checklist_item","clickup_create_comment","clickup_create_folder","clickup_create_list","clickup_create_task","clickup_create_time_entry","clickup_delete_checklist","clickup_delete_checklist_item","clickup_delete_comment","clickup_delete_task","clickup_delete_time_entry","clickup_get_comments","clickup_get_custom_fields","clickup_get_folders","clickup_get_list_members","clickup_get_lists","clickup_get_running_timer","clickup_get_space_tags","clickup_get_spaces","clickup_get_task","clickup_get_task_members","clickup_get_tasks","clickup_get_time_entries","clickup_get_workspaces","clickup_remove_custom_field_value","clickup_remove_tag_from_task","clickup_search_tasks","clickup_set_custom_field_value","clickup_start_timer","clickup_stop_timer","clickup_update_checklist","clickup_update_checklist_item","clickup_update_comment","clickup_update_task","clickup_update_time_entry","clickup_upload_attachment","cloudflare_create_access_application","cloudflare_create_access_policy","cloudflare_create_access_service_token","cloudflare_create_dns_record","cloudflare_create_r2_bucket","cloudflare_create_rate_limit_rule","cloudflare_create_ruleset","cloudflare_create_ruleset_rule","cloudflare_create_zone","cloudflare_delete_access_application","cloudflare_delete_access_policy","cloudflare_delete_dns_record","cloudflare_delete_r2_bucket","cloudflare_delete_ruleset_rule","cloudflare_delete_zone","cloudflare_dns_analytics","cloudflare_get_access_application","cloudflare_get_r2_bucket","cloudflare_get_ruleset","cloudflare_get_ruleset_entrypoint","cloudflare_get_tunnel","cloudflare_get_tunnel_configuration","cloudflare_get_worker_script_settings","cloudflare_get_zone","cloudflare_get_zone_settings","cloudflare_list_access_applications","cloudflare_list_access_groups","cloudflare_list_access_identity_providers","cloudflare_list_access_policies","cloudflare_list_access_service_tokens","cloudflare_list_certificates","cloudflare_list_dns_records","cloudflare_list_managed_ruleset_overrides","cloudflare_list_r2_buckets","cloudflare_list_rate_limit_rules","cloudflare_list_rulesets","cloudflare_list_tunnels","cloudflare_list_worker_routes","cloudflare_list_worker_scripts","cloudflare_list_zones","cloudflare_purge_cache","cloudflare_revoke_access_service_token","cloudflare_update_access_application","cloudflare_update_access_policy","cloudflare_update_dns_record","cloudflare_update_rate_limit_rule","cloudflare_update_ruleset_rule","cloudflare_update_zone_setting","cloudformation_cancel_update_stack","cloudformation_create_change_set","cloudformation_create_stack","cloudformation_delete_stack","cloudformation_describe_change_set","cloudformation_describe_stack_drift_detection_status","cloudformation_describe_stack_events","cloudformation_describe_stacks","cloudformation_detect_stack_drift","cloudformation_execute_change_set","cloudformation_get_template","cloudformation_get_template_summary","cloudformation_list_stack_resources","cloudformation_update_stack","cloudformation_validate_template","cloudwatch_describe_alarm_history","cloudwatch_describe_alarms","cloudwatch_describe_log_groups","cloudwatch_describe_log_streams","cloudwatch_filter_log_events","cloudwatch_get_log_events","cloudwatch_get_metric_statistics","cloudwatch_list_metrics","cloudwatch_mute_alarm","cloudwatch_put_log_group_retention","cloudwatch_put_metric_data","cloudwatch_query_logs","cloudwatch_unmute_alarm","codepipeline_disable_stage_transition","codepipeline_enable_stage_transition","codepipeline_get_pipeline","codepipeline_get_pipeline_execution","codepipeline_get_pipeline_state","codepipeline_list_action_executions","codepipeline_list_pipeline_executions","codepipeline_list_pipelines","codepipeline_put_approval_result","codepipeline_retry_stage_execution","codepipeline_start_execution","codepipeline_stop_execution","confluence_add_label","confluence_create_blogpost","confluence_create_comment","confluence_create_page","confluence_create_page_property","confluence_create_space","confluence_create_space_property","confluence_delete_attachment","confluence_delete_blogpost","confluence_delete_comment","confluence_delete_label","confluence_delete_page","confluence_delete_page_property","confluence_delete_space","confluence_delete_space_property","confluence_get_blogpost","confluence_get_page_ancestors","confluence_get_page_children","confluence_get_page_descendants","confluence_get_page_version","confluence_get_pages_by_label","confluence_get_space","confluence_get_task","confluence_get_user","confluence_list_attachments","confluence_list_blogposts","confluence_list_blogposts_in_space","confluence_list_comments","confluence_list_labels","confluence_list_page_properties","confluence_list_page_versions","confluence_list_pages_in_space","confluence_list_space_labels","confluence_list_space_permissions","confluence_list_space_properties","confluence_list_spaces","confluence_list_tasks","confluence_retrieve","confluence_search","confluence_search_in_space","confluence_update","confluence_update_blogpost","confluence_update_comment","confluence_update_space","confluence_update_task","confluence_upload_attachment","context_dev_classify_naics","context_dev_classify_sic","context_dev_crawl","context_dev_extract","context_dev_extract_product","context_dev_extract_products","context_dev_get_brand","context_dev_get_brand_by_email","context_dev_get_brand_by_name","context_dev_get_brand_by_ticker","context_dev_identify_transaction","context_dev_map","context_dev_scrape_fonts","context_dev_scrape_html","context_dev_scrape_images","context_dev_scrape_markdown","context_dev_scrape_styleguide","context_dev_screenshot","context_dev_search","convex_action","convex_document_deltas","convex_list_documents","convex_list_tables","convex_mutation","convex_query","convex_run_function","crowdstrike_create_indicators","crowdstrike_delete_indicators","crowdstrike_delete_rtr_session","crowdstrike_execute_rtr_command","crowdstrike_get_alert_details","crowdstrike_get_case_details","crowdstrike_get_host_group_details","crowdstrike_get_indicator_details","crowdstrike_get_rtr_command_status","crowdstrike_get_sensor_aggregates","crowdstrike_get_sensor_details","crowdstrike_get_vulnerability_details","crowdstrike_init_rtr_session","crowdstrike_perform_host_action","crowdstrike_perform_host_group_action","crowdstrike_query_alerts","crowdstrike_query_cases","crowdstrike_query_host_groups","crowdstrike_query_indicators","crowdstrike_query_sensors","crowdstrike_query_vulnerabilities","crowdstrike_update_alerts","crowdstrike_update_indicators","crunchbase_autocomplete","crunchbase_get_acquisition","crunchbase_get_entity","crunchbase_get_entity_card","crunchbase_get_fields_metadata","crunchbase_get_funding_round","crunchbase_get_organization","crunchbase_get_person","crunchbase_list_deleted_entities","crunchbase_search_acquisitions","crunchbase_search_entities","crunchbase_search_funding_rounds","crunchbase_search_organizations","crunchbase_search_people","cursor_add_followup","cursor_add_followup_v2","cursor_delete_agent","cursor_delete_agent_v2","cursor_download_artifact","cursor_download_artifact_v2","cursor_get_agent","cursor_get_agent_v2","cursor_get_api_key_info","cursor_get_api_key_info_v2","cursor_get_conversation","cursor_get_conversation_v2","cursor_launch_agent","cursor_launch_agent_v2","cursor_list_agents","cursor_list_agents_v2","cursor_list_artifacts","cursor_list_artifacts_v2","cursor_list_models","cursor_list_models_v2","cursor_list_repositories","cursor_list_repositories_v2","cursor_stop_agent","cursor_stop_agent_v2","dagster_delete_run","dagster_get_asset","dagster_get_run","dagster_get_run_logs","dagster_launch_run","dagster_list_assets","dagster_list_jobs","dagster_list_runs","dagster_list_schedules","dagster_list_sensors","dagster_materialize_assets","dagster_reexecute_run","dagster_report_asset_materialization","dagster_start_schedule","dagster_start_sensor","dagster_stop_schedule","dagster_stop_sensor","dagster_terminate_run","dagster_wipe_asset","databricks_cancel_run","databricks_execute_sql","databricks_get_cluster","databricks_get_job","databricks_get_run","databricks_get_run_output","databricks_get_statement","databricks_list_clusters","databricks_list_jobs","databricks_list_runs","databricks_list_warehouses","databricks_run_job","datadog_add_incident_todo","datadog_cancel_downtime","datadog_create_dashboard","datadog_create_downtime","datadog_create_event","datadog_create_incident","datadog_create_monitor","datadog_create_slo","datadog_delete_dashboard","datadog_delete_slo","datadog_get_browser_synthetics_results","datadog_get_dashboard","datadog_get_incident","datadog_get_monitor","datadog_get_security_signal","datadog_get_slo","datadog_get_slo_history","datadog_get_synthetics_results","datadog_get_synthetics_test","datadog_list_dashboards","datadog_list_downtimes","datadog_list_incidents","datadog_list_monitors","datadog_list_security_rules","datadog_list_security_signals","datadog_list_services","datadog_list_slos","datadog_list_synthetics_tests","datadog_mute_monitor","datadog_query_logs","datadog_query_timeseries","datadog_search_spans","datadog_send_logs","datadog_submit_metrics","datadog_trigger_synthetics_tests","datadog_unmute_monitor","datadog_update_incident","datadog_update_security_signal_assignee","datadog_update_security_signal_state","datadog_update_slo","datadog_update_synthetics_status","datagma_enrich_company","datagma_enrich_person","datagma_find_email","datagma_find_phone","datagma_get_credits","daytona_create_sandbox","daytona_delete_sandbox","daytona_download_file","daytona_execute_command","daytona_get_sandbox","daytona_git_clone","daytona_list_files","daytona_list_sandboxes","daytona_run_code","daytona_start_sandbox","daytona_stop_sandbox","daytona_upload_file","deployed_block_executor","deployments_deploy","deployments_get_version","deployments_list_versions","deployments_promote","deployments_undeploy","devin_append_session_tags","devin_archive_session","devin_create_session","devin_get_session","devin_get_session_tags","devin_list_session_attachments","devin_list_session_messages","devin_list_sessions","devin_replace_session_tags","devin_send_message","devin_terminate_session","discord_add_reaction","discord_archive_thread","discord_assign_role","discord_ban_member","discord_bulk_delete_messages","discord_create_channel","discord_create_invite","discord_create_role","discord_create_thread","discord_create_webhook","discord_delete_channel","discord_delete_invite","discord_delete_message","discord_delete_role","discord_delete_webhook","discord_edit_message","discord_execute_webhook","discord_get_channel","discord_get_invite","discord_get_member","discord_get_messages","discord_get_pinned_messages","discord_get_server","discord_get_user","discord_get_webhook","discord_join_thread","discord_kick_member","discord_leave_thread","discord_list_channels","discord_list_roles","discord_pin_message","discord_remove_reaction","discord_remove_role","discord_send_message","discord_unban_member","discord_unpin_message","discord_update_channel","discord_update_member","discord_update_role","docusign_create_from_template","docusign_download_document","docusign_get_envelope","docusign_list_envelopes","docusign_list_recipients","docusign_list_templates","docusign_send_envelope","docusign_void_envelope","downdetector_get_company","downdetector_get_company_attribution","downdetector_get_company_baseline","downdetector_get_company_events","downdetector_get_company_incidents","downdetector_get_company_indicators","downdetector_get_company_last_15","downdetector_get_company_status","downdetector_get_provider","downdetector_get_reports","downdetector_get_site_companies","downdetector_list_categories","downdetector_list_incidents","downdetector_list_sites","downdetector_search_companies","dropbox_copy","dropbox_create_folder","dropbox_create_shared_link","dropbox_delete","dropbox_download","dropbox_get_metadata","dropbox_list_folder","dropbox_list_revisions","dropbox_list_shared_links","dropbox_move","dropbox_restore","dropbox_search","dropbox_upload","dropcontact_enrich_contact","dspy_chain_of_thought","dspy_predict","dspy_react","dub_bulk_create_links","dub_bulk_delete_links","dub_bulk_update_links","dub_create_link","dub_create_tag","dub_delete_link","dub_get_analytics","dub_get_events","dub_get_link","dub_get_links_count","dub_get_qr_code","dub_list_domains","dub_list_folders","dub_list_links","dub_list_tags","dub_update_link","dub_upsert_link","duckduckgo_search","dynamodb_delete","dynamodb_get","dynamodb_introspect","dynamodb_put","dynamodb_query","dynamodb_scan","dynamodb_update","dynatrace_add_problem_comment","dynatrace_add_tags","dynatrace_close_problem","dynatrace_create_settings_object","dynatrace_create_slo","dynatrace_delete_problem_comment","dynatrace_delete_settings_object","dynatrace_delete_slo","dynatrace_delete_tag","dynatrace_execute_synthetic_monitors","dynatrace_get_attack","dynatrace_get_audit_logs","dynatrace_get_entity","dynatrace_get_event","dynatrace_get_metric","dynatrace_get_problem","dynatrace_get_problem_comment","dynatrace_get_security_problem","dynatrace_get_settings_object","dynatrace_get_slo","dynatrace_get_synthetic_batch","dynatrace_ingest_event","dynatrace_ingest_logs","dynatrace_ingest_metrics","dynatrace_list_attacks","dynatrace_list_entities","dynatrace_list_entity_types","dynatrace_list_events","dynatrace_list_metrics","dynatrace_list_problem_comments","dynatrace_list_problems","dynatrace_list_remediation_items","dynatrace_list_security_problems","dynatrace_list_settings_objects","dynatrace_list_settings_schemas","dynatrace_list_slos","dynatrace_list_synthetic_monitors","dynatrace_list_tags","dynatrace_mute_security_problem","dynatrace_mute_security_problems","dynatrace_query_metrics","dynatrace_search_logs","dynatrace_unmute_security_problem","dynatrace_unmute_security_problems","dynatrace_update_problem_comment","dynatrace_update_settings_object","dynatrace_update_slo","elasticsearch_bulk","elasticsearch_cluster_health","elasticsearch_cluster_stats","elasticsearch_count","elasticsearch_create_index","elasticsearch_delete_document","elasticsearch_delete_index","elasticsearch_get_document","elasticsearch_get_index","elasticsearch_index_document","elasticsearch_list_indices","elasticsearch_search","elasticsearch_update_document","elevenlabs_audio_isolation","elevenlabs_edit_voice_settings","elevenlabs_get_user","elevenlabs_get_voice","elevenlabs_get_voice_settings","elevenlabs_list_models","elevenlabs_list_voices","elevenlabs_sound_effects","elevenlabs_speech_to_speech","elevenlabs_tts","emailbison_attach_leads_to_campaign","emailbison_attach_tags_to_leads","emailbison_create_campaign","emailbison_create_lead","emailbison_create_tag","emailbison_get_lead","emailbison_list_campaigns","emailbison_list_leads","emailbison_list_replies","emailbison_list_tags","emailbison_update_campaign","emailbison_update_campaign_status","emailbison_update_lead","embeddings_cohere","embeddings_gemini","embeddings_mistral","embeddings_openai","embeddings_openrouter","enrich_check_credits","enrich_company_funding","enrich_company_lookup","enrich_company_revenue","enrich_disposable_email_check","enrich_email_to_ip","enrich_email_to_person_lite","enrich_email_to_phone","enrich_email_to_profile","enrich_find_email","enrich_get_post_details","enrich_ip_to_company","enrich_linkedin_profile","enrich_linkedin_to_personal_email","enrich_linkedin_to_work_email","enrich_phone_finder","enrich_reverse_hash_lookup","enrich_sales_pointer_people","enrich_search_company","enrich_search_company_activities","enrich_search_company_employees","enrich_search_jobs","enrich_search_logo","enrich_search_people","enrich_search_people_activities","enrich_search_post_comments","enrich_search_post_comments_by_url","enrich_search_post_reactions","enrich_search_post_reactions_by_url","enrich_search_posts","enrich_search_similar_companies","enrich_verify_email","enrichment_run","enrow_find_email","enrow_verify_email","exa_agent","exa_answer","exa_find_similar_links","exa_get_contents","exa_search","extend_parser","extend_parser_v2","fathom_get_summary","fathom_get_transcript","fathom_list_meeting_types","fathom_list_meetings","fathom_list_team_members","fathom_list_teams","file_append","file_compress","file_create_folder","file_decompress","file_delete_folder","file_edit","file_fetch","file_get","file_get_content","file_list","file_manage_sharing","file_move","file_parser","file_parser_v2","file_parser_v3","file_read","file_restore_folder","file_search","file_update_folder","file_write","findymail_find_email_from_linkedin","findymail_find_email_from_name","findymail_find_emails_by_domain","findymail_find_employees","findymail_find_phone","findymail_get_company","findymail_get_credits","findymail_lookup_technologies","findymail_reverse_email_lookup","findymail_search_technologies","findymail_verify_email","firecrawl_agent","firecrawl_batch_scrape","firecrawl_batch_scrape_status","firecrawl_cancel_crawl","firecrawl_crawl","firecrawl_crawl_status","firecrawl_credit_usage","firecrawl_extract","firecrawl_extract_status","firecrawl_map","firecrawl_parse","firecrawl_scrape","firecrawl_search","fireflies_add_to_live_meeting","fireflies_create_bite","fireflies_delete_transcript","fireflies_get_transcript","fireflies_get_user","fireflies_list_bites","fireflies_list_contacts","fireflies_list_transcripts","fireflies_list_users","fireflies_upload_audio","flint_create_task","flint_generate_pages","flint_get_task","function_execute","gamma_check_status","gamma_generate","gamma_generate_from_template","gamma_list_folders","gamma_list_themes","github_add_assignees","github_add_assignees_v2","github_add_labels","github_add_labels_v2","github_cancel_workflow_run","github_cancel_workflow_run_v2","github_check_star","github_check_star_v2","github_close_issue","github_close_issue_v2","github_close_pr","github_close_pr_v2","github_comment","github_comment_v2","github_compare_commits","github_compare_commits_v2","github_create_branch","github_create_branch_v2","github_create_comment_reaction","github_create_comment_reaction_v2","github_create_file","github_create_file_v2","github_create_gist","github_create_gist_v2","github_create_issue","github_create_issue_reaction","github_create_issue_reaction_v2","github_create_issue_v2","github_create_milestone","github_create_milestone_v2","github_create_pr","github_create_pr_review","github_create_pr_review_v2","github_create_pr_v2","github_create_project","github_create_project_v2","github_create_release","github_create_release_v2","github_delete_branch","github_delete_branch_v2","github_delete_comment","github_delete_comment_reaction","github_delete_comment_reaction_v2","github_delete_comment_v2","github_delete_file","github_delete_file_v2","github_delete_gist","github_delete_gist_v2","github_delete_issue_reaction","github_delete_issue_reaction_v2","github_delete_milestone","github_delete_milestone_v2","github_delete_project","github_delete_project_v2","github_delete_release","github_delete_release_v2","github_fork_gist","github_fork_gist_v2","github_fork_repo","github_fork_repo_v2","github_get_branch","github_get_branch_protection","github_get_branch_protection_v2","github_get_branch_v2","github_get_commit","github_get_commit_v2","github_get_file_content","github_get_file_content_v2","github_get_gist","github_get_gist_v2","github_get_issue","github_get_issue_v2","github_get_latest_release","github_get_latest_release_v2","github_get_milestone","github_get_milestone_v2","github_get_pr_files","github_get_pr_files_v2","github_get_project","github_get_project_v2","github_get_readme","github_get_readme_v2","github_get_release","github_get_release_v2","github_get_tree","github_get_tree_v2","github_get_workflow","github_get_workflow_run","github_get_workflow_run_v2","github_get_workflow_v2","github_issue_comment","github_issue_comment_v2","github_job_logs","github_latest_commit","github_latest_commit_v2","github_list_branches","github_list_branches_v2","github_list_commits","github_list_commits_v2","github_list_forks","github_list_forks_v2","github_list_gists","github_list_gists_v2","github_list_issue_comments","github_list_issue_comments_v2","github_list_issues","github_list_issues_v2","github_list_milestones","github_list_milestones_v2","github_list_pr_comments","github_list_pr_comments_v2","github_list_projects","github_list_projects_v2","github_list_prs","github_list_prs_v2","github_list_releases","github_list_releases_v2","github_list_review_threads","github_list_stargazers","github_list_stargazers_v2","github_list_tags","github_list_tags_v2","github_list_workflow_runs","github_list_workflow_runs_v2","github_list_workflows","github_list_workflows_v2","github_merge_pr","github_merge_pr_v2","github_pr","github_pr_v2","github_remove_label","github_remove_label_v2","github_reply_review_thread","github_repo_info","github_repo_info_v2","github_request_reviewers","github_request_reviewers_v2","github_rerun_workflow","github_rerun_workflow_v2","github_resolve_review_thread","github_search_code","github_search_code_v2","github_search_commits","github_search_commits_v2","github_search_issues","github_search_issues_v2","github_search_repos","github_search_repos_v2","github_search_users","github_search_users_v2","github_star_gist","github_star_gist_v2","github_star_repo","github_star_repo_v2","github_status_check_rollup","github_trigger_workflow","github_trigger_workflow_v2","github_unstar_gist","github_unstar_gist_v2","github_unstar_repo","github_unstar_repo_v2","github_update_branch_protection","github_update_branch_protection_v2","github_update_comment","github_update_comment_v2","github_update_file","github_update_file_v2","github_update_gist","github_update_gist_v2","github_update_issue","github_update_issue_v2","github_update_milestone","github_update_milestone_v2","github_update_pr","github_update_pr_v2","github_update_project","github_update_project_v2","github_update_release","github_update_release_v2","gitlab_activate_user","gitlab_add_member","gitlab_add_saml_group_link","gitlab_approve_access_request","gitlab_approve_merge_request","gitlab_approve_user","gitlab_ban_user","gitlab_block_user","gitlab_cancel_pipeline","gitlab_compare_branches","gitlab_create_branch","gitlab_create_file","gitlab_create_issue","gitlab_create_issue_note","gitlab_create_merge_request","gitlab_create_merge_request_note","gitlab_create_pipeline","gitlab_create_release","gitlab_create_user","gitlab_deactivate_user","gitlab_delete_branch","gitlab_delete_issue","gitlab_delete_saml_group_link","gitlab_delete_user","gitlab_delete_user_identity","gitlab_deny_access_request","gitlab_get_file","gitlab_get_group","gitlab_get_issue","gitlab_get_job_log","gitlab_get_merge_request","gitlab_get_merge_request_changes","gitlab_get_pipeline","gitlab_get_project","gitlab_invite_member","gitlab_list_access_requests","gitlab_list_branches","gitlab_list_commits","gitlab_list_groups","gitlab_list_invitations","gitlab_list_issues","gitlab_list_members","gitlab_list_merge_requests","gitlab_list_pipeline_jobs","gitlab_list_pipelines","gitlab_list_projects","gitlab_list_releases","gitlab_list_repository_tree","gitlab_list_saml_group_links","gitlab_list_user_memberships","gitlab_merge_merge_request","gitlab_play_job","gitlab_reject_user","gitlab_remove_member","gitlab_retry_pipeline","gitlab_revoke_invitation","gitlab_search_users","gitlab_unban_user","gitlab_unblock_user","gitlab_update_file","gitlab_update_invitation","gitlab_update_issue","gitlab_update_member","gitlab_update_merge_request","gitlab_update_user","gmail_add_label","gmail_add_label_v2","gmail_archive","gmail_archive_v2","gmail_create_label_v2","gmail_delete","gmail_delete_draft_v2","gmail_delete_label_v2","gmail_delete_v2","gmail_draft","gmail_draft_v2","gmail_edit_draft_v2","gmail_get_draft_v2","gmail_get_thread_v2","gmail_list_drafts_v2","gmail_list_labels_v2","gmail_list_threads_v2","gmail_mark_read","gmail_mark_read_v2","gmail_mark_unread","gmail_mark_unread_v2","gmail_move","gmail_move_v2","gmail_read","gmail_read_v2","gmail_remove_label","gmail_remove_label_v2","gmail_search","gmail_search_v2","gmail_send","gmail_send_v2","gmail_trash_thread_v2","gmail_unarchive","gmail_unarchive_v2","gmail_untrash_thread_v2","gmail_update_label_v2","gong_aggregate_activity","gong_aggregate_by_period","gong_answered_scorecards","gong_ask_anything","gong_assign_flow_prospects","gong_create_call","gong_day_by_day_activity","gong_get_brief","gong_get_call","gong_get_call_transcript","gong_get_coaching","gong_get_extensive_calls","gong_get_folder_content","gong_get_logs","gong_get_prospect_flows","gong_get_user","gong_interaction_stats","gong_list_calls","gong_list_flows","gong_list_library_folders","gong_list_scorecards","gong_list_trackers","gong_list_users","gong_list_workspaces","gong_lookup_email","gong_lookup_phone","gong_purge_email_address","gong_purge_phone_number","gong_unassign_flow_prospects","google_ads_ad_performance","google_ads_campaign_performance","google_ads_list_ad_groups","google_ads_list_campaigns","google_ads_list_customers","google_ads_search","google_appsheet_add_rows","google_appsheet_delete_rows","google_appsheet_edit_rows","google_appsheet_find_rows","google_bigquery_create_dataset","google_bigquery_create_table","google_bigquery_delete_dataset","google_bigquery_delete_table","google_bigquery_get_query_results","google_bigquery_get_table","google_bigquery_insert_rows","google_bigquery_list_datasets","google_bigquery_list_table_data","google_bigquery_list_tables","google_bigquery_query","google_books_volume_details","google_books_volume_search","google_calendar_create","google_calendar_create_calendar","google_calendar_create_calendar_v2","google_calendar_create_v2","google_calendar_delete","google_calendar_delete_calendar","google_calendar_delete_calendar_v2","google_calendar_delete_v2","google_calendar_freebusy","google_calendar_freebusy_v2","google_calendar_get","google_calendar_get_v2","google_calendar_instances","google_calendar_instances_v2","google_calendar_invite","google_calendar_invite_v2","google_calendar_list","google_calendar_list_acl","google_calendar_list_acl_v2","google_calendar_list_calendars","google_calendar_list_calendars_v2","google_calendar_list_v2","google_calendar_move","google_calendar_move_v2","google_calendar_quick_add","google_calendar_quick_add_v2","google_calendar_share_calendar","google_calendar_share_calendar_v2","google_calendar_unshare_calendar","google_calendar_unshare_calendar_v2","google_calendar_update","google_calendar_update_acl","google_calendar_update_acl_v2","google_calendar_update_calendar","google_calendar_update_calendar_v2","google_calendar_update_v2","google_contacts_create","google_contacts_delete","google_contacts_get","google_contacts_list","google_contacts_search","google_contacts_update","google_docs_create","google_docs_create_named_range","google_docs_create_paragraph_bullets","google_docs_delete_content_range","google_docs_delete_named_range","google_docs_delete_paragraph_bullets","google_docs_insert_image","google_docs_insert_page_break","google_docs_insert_table","google_docs_insert_text","google_docs_read","google_docs_replace_text","google_docs_update_paragraph_style","google_docs_update_text_style","google_docs_write","google_drive_copy","google_drive_create_comment","google_drive_create_folder","google_drive_delete","google_drive_delete_comment","google_drive_download","google_drive_export","google_drive_get_about","google_drive_get_content","google_drive_get_file","google_drive_get_revision","google_drive_list","google_drive_list_comments","google_drive_list_permissions","google_drive_list_revisions","google_drive_move","google_drive_search","google_drive_share","google_drive_trash","google_drive_unshare","google_drive_untrash","google_drive_update","google_drive_upload","google_forms_batch_update","google_forms_create_form","google_forms_create_watch","google_forms_delete_watch","google_forms_get_form","google_forms_get_responses","google_forms_list_watches","google_forms_renew_watch","google_forms_set_publish_settings","google_groups_add_alias","google_groups_add_member","google_groups_create_group","google_groups_delete_group","google_groups_get_group","google_groups_get_member","google_groups_get_settings","google_groups_has_member","google_groups_list_aliases","google_groups_list_groups","google_groups_list_members","google_groups_remove_alias","google_groups_remove_member","google_groups_update_group","google_groups_update_member","google_groups_update_settings","google_maps_air_quality","google_maps_directions","google_maps_distance_matrix","google_maps_elevation","google_maps_geocode","google_maps_geolocate","google_maps_place_details","google_maps_places_nearby","google_maps_places_search","google_maps_pollen","google_maps_reverse_geocode","google_maps_snap_to_roads","google_maps_solar","google_maps_speed_limits","google_maps_timezone","google_maps_validate_address","google_meet_create_space","google_meet_end_conference","google_meet_get_conference_record","google_meet_get_space","google_meet_list_conference_records","google_meet_list_participants","google_pagespeed_analyze","google_search","google_sheets_append","google_sheets_append_v2","google_sheets_batch_clear_v2","google_sheets_batch_get_v2","google_sheets_batch_update_v2","google_sheets_clear_v2","google_sheets_copy_sheet_v2","google_sheets_create_spreadsheet_v2","google_sheets_delete_rows_v2","google_sheets_delete_sheet_v2","google_sheets_delete_spreadsheet_v2","google_sheets_get_spreadsheet_v2","google_sheets_read","google_sheets_read_v2","google_sheets_update","google_sheets_update_v2","google_sheets_write","google_sheets_write_v2","google_slides_add_image","google_slides_add_slide","google_slides_batch_update","google_slides_copy_presentation","google_slides_create","google_slides_create_line","google_slides_create_paragraph_bullets","google_slides_create_shape","google_slides_create_sheets_chart","google_slides_create_table","google_slides_create_video","google_slides_delete_object","google_slides_delete_paragraph_bullets","google_slides_delete_table_column","google_slides_delete_table_row","google_slides_delete_text","google_slides_duplicate_object","google_slides_export_presentation","google_slides_get_page","google_slides_get_thumbnail","google_slides_group_objects","google_slides_insert_table_columns","google_slides_insert_table_rows","google_slides_insert_text","google_slides_merge_table_cells","google_slides_read","google_slides_refresh_sheets_chart","google_slides_replace_all_shapes_with_image","google_slides_replace_all_shapes_with_sheets_chart","google_slides_replace_all_text","google_slides_replace_image","google_slides_reroute_line","google_slides_ungroup_objects","google_slides_unmerge_table_cells","google_slides_update_image_properties","google_slides_update_line_category","google_slides_update_line_properties","google_slides_update_page_element_alt_text","google_slides_update_page_element_transform","google_slides_update_page_elements_z_order","google_slides_update_page_properties","google_slides_update_paragraph_style","google_slides_update_shape_properties","google_slides_update_slide_properties","google_slides_update_slides_position","google_slides_update_table_border_properties","google_slides_update_table_cell_properties","google_slides_update_table_column_properties","google_slides_update_table_row_properties","google_slides_update_text_style","google_slides_update_video_properties","google_slides_write","google_tasks_create","google_tasks_delete","google_tasks_get","google_tasks_list","google_tasks_list_task_lists","google_tasks_update","google_translate_detect","google_translate_text","google_vault_add_held_accounts","google_vault_add_matters_permissions","google_vault_close_matters","google_vault_create_matters","google_vault_create_matters_export","google_vault_create_matters_holds","google_vault_create_saved_query","google_vault_delete_matters","google_vault_delete_matters_export","google_vault_delete_matters_holds","google_vault_delete_saved_query","google_vault_download_export_file","google_vault_list_matters","google_vault_list_matters_export","google_vault_list_matters_holds","google_vault_list_saved_queries","google_vault_remove_held_accounts","google_vault_remove_matters_permissions","google_vault_reopen_matters","google_vault_undelete_matters","google_vault_update_matters","google_vault_update_matters_holds","grafana_check_data_source_health","grafana_create_alert_rule","grafana_create_annotation","grafana_create_contact_point","grafana_create_dashboard","grafana_create_folder","grafana_delete_alert_rule","grafana_delete_annotation","grafana_delete_contact_point","grafana_delete_dashboard","grafana_delete_folder","grafana_get_alert_rule","grafana_get_alert_rule_group","grafana_get_dashboard","grafana_get_data_source","grafana_get_folder","grafana_get_health","grafana_list_alert_rules","grafana_list_annotations","grafana_list_contact_points","grafana_list_dashboards","grafana_list_data_sources","grafana_list_folders","grafana_move_folder","grafana_query_data_source","grafana_update_alert_rule","grafana_update_annotation","grafana_update_contact_point","grafana_update_dashboard","grafana_update_folder","grain_create_hook","grain_create_hook_v2","grain_delete_hook","grain_delete_hook_v2","grain_get_recording","grain_get_transcript","grain_list_hooks","grain_list_hooks_v2","grain_list_meeting_types","grain_list_recordings","grain_list_teams","grain_list_views","granola_create_webhook_endpoint","granola_delete_webhook_endpoint","granola_get_note","granola_get_transcript","granola_list_audit_events","granola_list_folders","granola_list_notes","granola_list_webhook_endpoints","granola_update_webhook_endpoint","greenhouse_get_application","greenhouse_get_candidate","greenhouse_get_job","greenhouse_get_user","greenhouse_list_applications","greenhouse_list_candidates","greenhouse_list_departments","greenhouse_list_job_stages","greenhouse_list_jobs","greenhouse_list_offices","greenhouse_list_users","greptile_index_repo","greptile_query","greptile_search","greptile_status","guardrails_validate","harmonic_batch_get_people","harmonic_clear_people_saved_search_net_new_results","harmonic_enrich_person","harmonic_get_company_employees","harmonic_get_email_enrichment_job","harmonic_get_email_enrichment_usage","harmonic_get_enrichment_status","harmonic_get_people_saved_search_net_new_results","harmonic_get_people_saved_search_results","harmonic_get_person","harmonic_list_people_saved_searches","harmonic_search_people_scout","harmonic_submit_email_enrichment_job","hex_cancel_run","hex_create_collection","hex_create_group","hex_deactivate_user","hex_delete_group","hex_get_collection","hex_get_data_connection","hex_get_group","hex_get_project","hex_get_project_runs","hex_get_queried_tables","hex_get_run_status","hex_list_collections","hex_list_data_connections","hex_list_groups","hex_list_projects","hex_list_users","hex_run_project","hex_update_collection","hex_update_group","hex_update_project","http_request","hubspot_add_list_memberships","hubspot_create_appointment","hubspot_create_association","hubspot_create_company","hubspot_create_contact","hubspot_create_deal","hubspot_create_email","hubspot_create_line_item","hubspot_create_list","hubspot_create_note","hubspot_create_ticket","hubspot_delete_association","hubspot_delete_company","hubspot_delete_contact","hubspot_delete_deal","hubspot_delete_line_item","hubspot_delete_ticket","hubspot_get_appointment","hubspot_get_association_labels","hubspot_get_cart","hubspot_get_company","hubspot_get_contact","hubspot_get_deal","hubspot_get_email","hubspot_get_line_item","hubspot_get_list","hubspot_get_list_memberships","hubspot_get_marketing_event","hubspot_get_note","hubspot_get_properties","hubspot_get_quote","hubspot_get_ticket","hubspot_get_users","hubspot_list_appointments","hubspot_list_associations","hubspot_list_carts","hubspot_list_companies","hubspot_list_contacts","hubspot_list_deals","hubspot_list_emails","hubspot_list_line_items","hubspot_list_lists","hubspot_list_marketing_events","hubspot_list_notes","hubspot_list_owners","hubspot_list_quotes","hubspot_list_tickets","hubspot_remove_list_memberships","hubspot_search_companies","hubspot_search_contacts","hubspot_search_deals","hubspot_search_emails","hubspot_search_line_items","hubspot_search_notes","hubspot_search_quotes","hubspot_search_tickets","hubspot_update_appointment","hubspot_update_company","hubspot_update_contact","hubspot_update_deal","hubspot_update_line_item","hubspot_update_ticket","huggingface_chat","hunter_companies_find","hunter_discover","hunter_domain_search","hunter_email_count","hunter_email_finder","hunter_email_verifier","iam_add_user_to_group","iam_attach_role_policy","iam_attach_user_policy","iam_create_access_key","iam_create_role","iam_create_user","iam_delete_access_key","iam_delete_role","iam_delete_user","iam_detach_role_policy","iam_detach_user_policy","iam_get_role","iam_get_user","iam_list_attached_role_policies","iam_list_attached_user_policies","iam_list_groups","iam_list_policies","iam_list_roles","iam_list_users","iam_remove_user_from_group","iam_simulate_principal_policy","icypeas_find_email","icypeas_verify_email","identity_center_check_assignment_deletion_status","identity_center_check_assignment_status","identity_center_create_account_assignment","identity_center_delete_account_assignment","identity_center_describe_account","identity_center_get_group","identity_center_get_user","identity_center_list_account_assignments","identity_center_list_accounts","identity_center_list_groups","identity_center_list_instances","identity_center_list_permission_sets","image_generate","incidentio_actions_create","incidentio_actions_list","incidentio_actions_show","incidentio_actions_update","incidentio_alert_events_create","incidentio_alerts_list","incidentio_alerts_resolve","incidentio_alerts_show","incidentio_catalog_entries_list","incidentio_catalog_types_list","incidentio_custom_fields_create","incidentio_custom_fields_delete","incidentio_custom_fields_list","incidentio_custom_fields_show","incidentio_custom_fields_update","incidentio_escalation_paths_create","incidentio_escalation_paths_delete","incidentio_escalation_paths_list","incidentio_escalation_paths_show","incidentio_escalation_paths_update","incidentio_escalations_cancel","incidentio_escalations_create","incidentio_escalations_list","incidentio_escalations_show","incidentio_follow_ups_create","incidentio_follow_ups_list","incidentio_follow_ups_show","incidentio_follow_ups_update","incidentio_incident_alerts_list","incidentio_incident_memberships_create","incidentio_incident_memberships_revoke","incidentio_incident_participants_list","incidentio_incident_roles_create","incidentio_incident_roles_delete","incidentio_incident_roles_list","incidentio_incident_roles_show","incidentio_incident_roles_update","incidentio_incident_statuses_list","incidentio_incident_timestamps_list","incidentio_incident_timestamps_show","incidentio_incident_types_list","incidentio_incident_updates_list","incidentio_incidents_create","incidentio_incidents_list","incidentio_incidents_show","incidentio_incidents_update","incidentio_on_call_now","incidentio_schedule_entries_list","incidentio_schedule_overrides_create","incidentio_schedule_overrides_list","incidentio_schedules_create","incidentio_schedules_delete","incidentio_schedules_list","incidentio_schedules_show","incidentio_schedules_update","incidentio_severities_list","incidentio_teams_list","incidentio_teams_show","incidentio_users_list","incidentio_users_show","incidentio_workflows_create","incidentio_workflows_delete","incidentio_workflows_list","incidentio_workflows_show","incidentio_workflows_update","infisical_create_secret","infisical_delete_secret","infisical_get_secret","infisical_list_secrets","infisical_update_secret","instagram_delete_comment","instagram_download_media","instagram_get_account_insights","instagram_get_container_status","instagram_get_conversation_messages","instagram_get_media","instagram_get_media_insights","instagram_get_message","instagram_get_profile","instagram_get_publishing_limit","instagram_hide_comment","instagram_list_comments","instagram_list_conversations","instagram_list_media","instagram_list_stories","instagram_private_reply","instagram_publish_carousel","instagram_publish_image","instagram_publish_reel","instagram_publish_story","instagram_publish_video","instagram_reply_to_comment","instagram_send_text_message","instagram_set_comments_enabled","instantly_activate_campaign","instantly_create_campaign","instantly_create_lead","instantly_create_lead_list","instantly_delete_campaign","instantly_delete_leads","instantly_get_lead","instantly_list_campaigns","instantly_list_emails","instantly_list_lead_lists","instantly_list_leads","instantly_patch_campaign","instantly_patch_lead","instantly_pause_campaign","instantly_reply_to_email","instantly_update_lead_interest_status","intercom_assign_conversation_v2","intercom_attach_contact_to_company_v2","intercom_close_conversation_v2","intercom_create_company","intercom_create_company_v2","intercom_create_contact","intercom_create_contact_v2","intercom_create_event_v2","intercom_create_message","intercom_create_message_v2","intercom_create_note_v2","intercom_create_tag_v2","intercom_create_ticket","intercom_create_ticket_v2","intercom_delete_contact","intercom_delete_contact_v2","intercom_detach_contact_from_company_v2","intercom_get_company","intercom_get_company_v2","intercom_get_contact","intercom_get_contact_v2","intercom_get_conversation","intercom_get_conversation_v2","intercom_get_ticket","intercom_get_ticket_v2","intercom_list_admins_v2","intercom_list_companies","intercom_list_companies_v2","intercom_list_contacts","intercom_list_contacts_v2","intercom_list_conversations","intercom_list_conversations_v2","intercom_list_tags_v2","intercom_open_conversation_v2","intercom_reply_conversation","intercom_reply_conversation_v2","intercom_search_contacts","intercom_search_contacts_v2","intercom_search_conversations","intercom_search_conversations_v2","intercom_snooze_conversation_v2","intercom_tag_contact_v2","intercom_tag_conversation_v2","intercom_untag_contact_v2","intercom_update_contact","intercom_update_contact_v2","intercom_update_ticket_v2","jina_read_url","jina_search","jira_add_attachment","jira_add_comment","jira_add_watcher","jira_add_worklog","jira_assign_issue","jira_bulk_read","jira_create_issue_link","jira_delete_attachment","jira_delete_comment","jira_delete_issue","jira_delete_issue_link","jira_delete_worklog","jira_get_attachments","jira_get_comments","jira_get_fields","jira_get_project","jira_get_transitions","jira_get_users","jira_get_worklogs","jira_list_issue_types","jira_list_projects","jira_remove_watcher","jira_retrieve","jira_search_issues","jira_search_users","jira_transition_issue","jira_update","jira_update_comment","jira_update_worklog","jira_write","jotform_add_label_resources","jotform_clone_form","jotform_create_form","jotform_create_label","jotform_create_question","jotform_create_questions","jotform_create_report","jotform_create_submission","jotform_create_submissions","jotform_create_webhook","jotform_delete_form","jotform_delete_label","jotform_delete_question","jotform_delete_report","jotform_delete_submission","jotform_delete_webhook","jotform_get_form","jotform_get_form_properties","jotform_get_history","jotform_get_label","jotform_get_question","jotform_get_report","jotform_get_settings","jotform_get_submission","jotform_get_usage","jotform_get_user","jotform_list_form_files","jotform_list_form_reports","jotform_list_form_submissions","jotform_list_forms","jotform_list_label_resources","jotform_list_labels","jotform_list_questions","jotform_list_reports","jotform_list_submissions","jotform_list_subusers","jotform_list_webhooks","jotform_remove_label_resources","jotform_update_form_properties","jotform_update_label","jotform_update_question","jotform_update_settings","jotform_update_submission","jsm_add_comment","jsm_add_customer","jsm_add_organization","jsm_add_participants","jsm_answer_approval","jsm_attach_form","jsm_copy_forms","jsm_create_object","jsm_create_organization","jsm_create_request","jsm_delete_form","jsm_delete_object","jsm_externalise_form","jsm_get_approvals","jsm_get_comments","jsm_get_customers","jsm_get_form","jsm_get_form_answers","jsm_get_form_structure","jsm_get_form_templates","jsm_get_issue_forms","jsm_get_object","jsm_get_object_schema","jsm_get_object_type_attributes","jsm_get_organizations","jsm_get_participants","jsm_get_queues","jsm_get_request","jsm_get_request_type_fields","jsm_get_request_types","jsm_get_requests","jsm_get_service_desks","jsm_get_sla","jsm_get_transitions","jsm_internalise_form","jsm_list_object_schemas","jsm_list_object_types","jsm_reopen_form","jsm_save_form_answers","jsm_search_objects_aql","jsm_submit_form","jsm_transition_request","jsm_update_object","jupyter_copy_content","jupyter_create_file","jupyter_create_session","jupyter_delete_content","jupyter_delete_session","jupyter_get_content","jupyter_interrupt_kernel","jupyter_list_contents","jupyter_list_kernels","jupyter_list_kernelspecs","jupyter_list_sessions","jupyter_rename_content","jupyter_restart_kernel","jupyter_start_kernel","jupyter_stop_kernel","jupyter_upload_file","kalshi_amend_order","kalshi_amend_order_v2","kalshi_cancel_order","kalshi_cancel_order_v2","kalshi_create_order","kalshi_create_order_v2","kalshi_get_balance","kalshi_get_balance_v2","kalshi_get_candlesticks","kalshi_get_candlesticks_v2","kalshi_get_event","kalshi_get_event_candlesticks","kalshi_get_event_candlesticks_v2","kalshi_get_event_v2","kalshi_get_events","kalshi_get_events_v2","kalshi_get_exchange_announcements","kalshi_get_exchange_announcements_v2","kalshi_get_exchange_schedule","kalshi_get_exchange_schedule_v2","kalshi_get_exchange_status","kalshi_get_exchange_status_v2","kalshi_get_fills","kalshi_get_fills_v2","kalshi_get_market","kalshi_get_market_v2","kalshi_get_markets","kalshi_get_markets_v2","kalshi_get_order","kalshi_get_order_v2","kalshi_get_orderbook","kalshi_get_orderbook_v2","kalshi_get_orders","kalshi_get_orders_v2","kalshi_get_positions","kalshi_get_positions_v2","kalshi_get_series_by_ticker","kalshi_get_series_by_ticker_v2","kalshi_get_series_list","kalshi_get_series_list_v2","kalshi_get_settlements","kalshi_get_settlements_v2","kalshi_get_trades","kalshi_get_trades_v2","ketch_get_consent","ketch_get_subscriptions","ketch_invoke_right","ketch_set_consent","ketch_set_subscriptions","knowledge_create_document","knowledge_delete_chunk","knowledge_delete_document","knowledge_get_connector","knowledge_get_document","knowledge_list_chunks","knowledge_list_connectors","knowledge_list_documents","knowledge_list_tags","knowledge_search","knowledge_trigger_sync","knowledge_update_chunk","knowledge_upload_chunk","knowledge_upsert_document","lambda_add_permission","lambda_create_alias","lambda_create_event_source_mapping","lambda_create_function","lambda_create_function_url_config","lambda_delete_alias","lambda_delete_event_source_mapping","lambda_delete_function","lambda_delete_function_concurrency","lambda_delete_function_event_invoke_config","lambda_delete_function_url_config","lambda_delete_provisioned_concurrency_config","lambda_get_account_settings","lambda_get_alias","lambda_get_event_source_mapping","lambda_get_function","lambda_get_function_concurrency","lambda_get_function_configuration","lambda_get_function_event_invoke_config","lambda_get_function_recursion_config","lambda_get_function_url_config","lambda_get_layer_version","lambda_get_policy","lambda_get_provisioned_concurrency_config","lambda_get_runtime_management_config","lambda_invoke","lambda_list_aliases","lambda_list_event_source_mappings","lambda_list_function_event_invoke_configs","lambda_list_function_url_configs","lambda_list_functions","lambda_list_layer_versions","lambda_list_layers","lambda_list_provisioned_concurrency_configs","lambda_list_tags","lambda_list_versions_by_function","lambda_publish_version","lambda_put_function_concurrency","lambda_put_function_event_invoke_config","lambda_put_function_recursion_config","lambda_put_provisioned_concurrency_config","lambda_put_runtime_management_config","lambda_remove_permission","lambda_tag_resource","lambda_untag_resource","lambda_update_alias","lambda_update_event_source_mapping","lambda_update_function_code","lambda_update_function_configuration","lambda_update_function_url_config","langsmith_create_feedback","langsmith_create_run","langsmith_create_runs_batch","langsmith_get_run","langsmith_update_run","latex_compile","latex_get_package","latex_list_fonts","latex_search_packages","launchdarkly_create_flag","launchdarkly_delete_flag","launchdarkly_get_audit_log","launchdarkly_get_flag","launchdarkly_get_flag_status","launchdarkly_list_environments","launchdarkly_list_flags","launchdarkly_list_members","launchdarkly_list_projects","launchdarkly_list_segments","launchdarkly_toggle_flag","launchdarkly_update_flag","leadmagic_company_search","leadmagic_email_to_profile","leadmagic_find_email","leadmagic_find_mobile","leadmagic_get_credits","leadmagic_profile_search","leadmagic_profile_to_email","leadmagic_role_finder","leadmagic_validate_email","lemlist_get_activities","lemlist_get_lead","lemlist_send_email","linear_add_label_to_issue","linear_add_label_to_project","linear_archive_issue","linear_archive_label","linear_archive_project","linear_create_attachment","linear_create_comment","linear_create_customer","linear_create_customer_request","linear_create_customer_status","linear_create_customer_tier","linear_create_cycle","linear_create_favorite","linear_create_issue","linear_create_issue_relation","linear_create_label","linear_create_project","linear_create_project_label","linear_create_project_milestone","linear_create_project_status","linear_create_project_update","linear_create_workflow_state","linear_delete_attachment","linear_delete_comment","linear_delete_customer","linear_delete_customer_status","linear_delete_customer_tier","linear_delete_issue","linear_delete_issue_relation","linear_delete_project","linear_delete_project_label","linear_delete_project_milestone","linear_delete_project_status","linear_get_active_cycle","linear_get_customer","linear_get_cycle","linear_get_issue","linear_get_project","linear_get_viewer","linear_list_attachments","linear_list_comments","linear_list_customer_requests","linear_list_customer_statuses","linear_list_customer_tiers","linear_list_customers","linear_list_cycles","linear_list_favorites","linear_list_issue_relations","linear_list_labels","linear_list_notifications","linear_list_project_labels","linear_list_project_milestones","linear_list_project_statuses","linear_list_project_updates","linear_list_projects","linear_list_teams","linear_list_users","linear_list_workflow_states","linear_merge_customers","linear_read_issues","linear_remove_label_from_issue","linear_remove_label_from_project","linear_search_issues","linear_unarchive_issue","linear_update_attachment","linear_update_comment","linear_update_customer","linear_update_customer_request","linear_update_customer_status","linear_update_customer_tier","linear_update_issue","linear_update_label","linear_update_notification","linear_update_project","linear_update_project_label","linear_update_project_milestone","linear_update_project_status","linear_update_workflow_state","linkedin_get_profile","linkedin_share_post","linkup_search","linq_add_participant","linq_check_imessage","linq_check_rcs","linq_create_attachment","linq_create_chat","linq_create_contact_card","linq_create_webhook_subscription","linq_delete_attachment","linq_delete_message","linq_delete_webhook_subscription","linq_edit_message","linq_get_attachment","linq_get_chat","linq_get_contact_card","linq_get_message","linq_get_webhook_subscription","linq_leave_chat","linq_list_chats","linq_list_messages","linq_list_phone_numbers","linq_list_thread","linq_list_webhook_events","linq_list_webhook_subscriptions","linq_mark_chat_read","linq_react_to_message","linq_remove_participant","linq_send_message","linq_send_voice_memo","linq_share_contact_card","linq_start_typing","linq_stop_typing","linq_update_chat","linq_update_contact_card","linq_update_webhook_subscription","llm_chat","logfire_get_token_info","logfire_get_trace","logfire_query","logfire_search_records","logrocket_create_release","logrocket_get_audit_logs","logrocket_get_highlights","logrocket_identify_user","logrocket_list_exported_sessions","logrocket_request_highlights","logs_get","logs_get_execution","logs_get_run_details","logs_query","logs_query_runs","loops_check_contact_suppression","loops_create_contact","loops_create_contact_property","loops_delete_contact","loops_find_contact","loops_get_transactional_email","loops_list_contact_properties","loops_list_mailing_lists","loops_list_transactional_emails","loops_remove_contact_suppression","loops_send_event","loops_send_transactional_email","loops_update_contact","luma_add_guests","luma_cancel_event","luma_create_event","luma_get_event","luma_get_guest","luma_get_guests","luma_list_events","luma_lookup_event","luma_send_invites","luma_update_event","luma_update_guest_status","mailchimp_add_member","mailchimp_add_member_tags","mailchimp_add_or_update_member","mailchimp_add_segment_member","mailchimp_add_subscriber_to_automation","mailchimp_archive_member","mailchimp_create_audience","mailchimp_create_batch_operation","mailchimp_create_campaign","mailchimp_create_interest","mailchimp_create_interest_category","mailchimp_create_landing_page","mailchimp_create_merge_field","mailchimp_create_segment","mailchimp_create_template","mailchimp_delete_audience","mailchimp_delete_batch_operation","mailchimp_delete_campaign","mailchimp_delete_interest","mailchimp_delete_interest_category","mailchimp_delete_landing_page","mailchimp_delete_member","mailchimp_delete_merge_field","mailchimp_delete_segment","mailchimp_delete_template","mailchimp_get_audience","mailchimp_get_audiences","mailchimp_get_automation","mailchimp_get_automations","mailchimp_get_batch_operation","mailchimp_get_batch_operations","mailchimp_get_campaign","mailchimp_get_campaign_content","mailchimp_get_campaign_report","mailchimp_get_campaign_reports","mailchimp_get_campaigns","mailchimp_get_interest","mailchimp_get_interest_categories","mailchimp_get_interest_category","mailchimp_get_interests","mailchimp_get_landing_page","mailchimp_get_landing_pages","mailchimp_get_member","mailchimp_get_member_tags","mailchimp_get_members","mailchimp_get_merge_field","mailchimp_get_merge_fields","mailchimp_get_segment","mailchimp_get_segment_members","mailchimp_get_segments","mailchimp_get_template","mailchimp_get_templates","mailchimp_pause_automation","mailchimp_publish_landing_page","mailchimp_remove_member_tags","mailchimp_remove_segment_member","mailchimp_replicate_campaign","mailchimp_schedule_campaign","mailchimp_send_campaign","mailchimp_set_campaign_content","mailchimp_start_automation","mailchimp_unarchive_member","mailchimp_unpublish_landing_page","mailchimp_unschedule_campaign","mailchimp_update_audience","mailchimp_update_campaign","mailchimp_update_interest","mailchimp_update_interest_category","mailchimp_update_landing_page","mailchimp_update_member","mailchimp_update_merge_field","mailchimp_update_segment","mailchimp_update_template","mailgun_add_list_member","mailgun_create_mailing_list","mailgun_get_domain","mailgun_get_mailing_list","mailgun_get_message","mailgun_list_domains","mailgun_list_messages","mailgun_send_message","managed_agent_archive_session","managed_agent_create_session","managed_agent_delete_session","managed_agent_get_session","managed_agent_interrupt_session","managed_agent_list_events","managed_agent_respond_custom_tool","managed_agent_respond_tool_confirmation","managed_agent_run_session","managed_agent_send_message","managed_agent_update_session","manageengine_sdp_add_change_note","manageengine_sdp_add_problem_note","manageengine_sdp_add_request_note","manageengine_sdp_create_asset","manageengine_sdp_create_change","manageengine_sdp_create_problem","manageengine_sdp_create_request","manageengine_sdp_create_solution","manageengine_sdp_delete_asset","manageengine_sdp_delete_change","manageengine_sdp_delete_problem","manageengine_sdp_delete_request","manageengine_sdp_delete_solution","manageengine_sdp_get_asset","manageengine_sdp_get_change","manageengine_sdp_get_problem","manageengine_sdp_get_request","manageengine_sdp_get_solution","manageengine_sdp_list_assets","manageengine_sdp_list_change_notes","manageengine_sdp_list_changes","manageengine_sdp_list_problem_notes","manageengine_sdp_list_problems","manageengine_sdp_list_request_notes","manageengine_sdp_list_requests","manageengine_sdp_list_solutions","manageengine_sdp_update_asset","manageengine_sdp_update_change","manageengine_sdp_update_problem","manageengine_sdp_update_request","manageengine_sdp_update_solution","mem0_add_memories","mem0_get_memories","mem0_search_memories","memory_add","memory_delete","memory_get","memory_get_all","microsoft_ad_add_directory_role_member","microsoft_ad_add_group_member","microsoft_ad_add_user_app_role_assignment","microsoft_ad_assign_license","microsoft_ad_create_group","microsoft_ad_create_user","microsoft_ad_delete_group","microsoft_ad_delete_user","microsoft_ad_get_conditional_access_policy","microsoft_ad_get_device","microsoft_ad_get_group","microsoft_ad_get_user","microsoft_ad_list_authentication_methods","microsoft_ad_list_conditional_access_policies","microsoft_ad_list_devices","microsoft_ad_list_directory_audits","microsoft_ad_list_directory_role_members","microsoft_ad_list_directory_roles","microsoft_ad_list_group_members","microsoft_ad_list_groups","microsoft_ad_list_service_principal_app_role_assignments","microsoft_ad_list_service_principals","microsoft_ad_list_sign_ins","microsoft_ad_list_subscribed_skus","microsoft_ad_list_user_app_role_assignments","microsoft_ad_list_user_devices","microsoft_ad_list_user_licenses","microsoft_ad_list_users","microsoft_ad_remove_directory_role_member","microsoft_ad_remove_group_member","microsoft_ad_remove_user_app_role_assignment","microsoft_ad_reset_password","microsoft_ad_revoke_sign_in_sessions","microsoft_ad_set_password","microsoft_ad_update_group","microsoft_ad_update_user","microsoft_dataverse_associate","microsoft_dataverse_create_multiple","microsoft_dataverse_create_record","microsoft_dataverse_delete_record","microsoft_dataverse_disassociate","microsoft_dataverse_download_file","microsoft_dataverse_execute_action","microsoft_dataverse_execute_function","microsoft_dataverse_fetchxml_query","microsoft_dataverse_get_entity_metadata","microsoft_dataverse_get_record","microsoft_dataverse_list_records","microsoft_dataverse_search","microsoft_dataverse_update_multiple","microsoft_dataverse_update_record","microsoft_dataverse_upload_file","microsoft_dataverse_upsert_record","microsoft_dataverse_whoami","microsoft_dynamics_365_close_case","microsoft_dynamics_365_close_opportunity","microsoft_dynamics_365_create_record","microsoft_dynamics_365_get_record","microsoft_dynamics_365_list_records","microsoft_dynamics_365_qualify_lead","microsoft_dynamics_365_search_records","microsoft_dynamics_365_update_record","microsoft_excel_clear_range","microsoft_excel_create_table","microsoft_excel_delete_worksheet","microsoft_excel_format_range","microsoft_excel_read","microsoft_excel_read_v2","microsoft_excel_sort_range","microsoft_excel_table_add","microsoft_excel_worksheet_add","microsoft_excel_write","microsoft_excel_write_v2","microsoft_planner_create_bucket","microsoft_planner_create_plan","microsoft_planner_create_task","microsoft_planner_delete_bucket","microsoft_planner_delete_plan","microsoft_planner_delete_task","microsoft_planner_get_plan_details","microsoft_planner_get_task_details","microsoft_planner_list_buckets","microsoft_planner_list_plans","microsoft_planner_read_bucket","microsoft_planner_read_plan","microsoft_planner_read_task","microsoft_planner_update_bucket","microsoft_planner_update_plan","microsoft_planner_update_plan_details","microsoft_planner_update_task","microsoft_planner_update_task_details","microsoft_teams_delete_channel_message","microsoft_teams_delete_chat_message","microsoft_teams_get_message","microsoft_teams_list_channel_members","microsoft_teams_list_channels","microsoft_teams_list_chat_members","microsoft_teams_list_chats","microsoft_teams_list_team_members","microsoft_teams_list_teams","microsoft_teams_read_channel","microsoft_teams_read_chat","microsoft_teams_reply_to_message","microsoft_teams_set_reaction","microsoft_teams_unset_reaction","microsoft_teams_update_channel_message","microsoft_teams_update_chat_message","microsoft_teams_write_channel","microsoft_teams_write_chat","microsoft_word_append","microsoft_word_create","microsoft_word_create_from_template","microsoft_word_export_pdf","microsoft_word_list","microsoft_word_read","microsoft_word_replace_text","microsoft_word_update","millionverifier_get_credits","millionverifier_verify_email","mintlify_create_agent_job","mintlify_create_assistant_message","mintlify_detect_ai_prose","mintlify_get_agent_job","mintlify_get_assistant_caller_stats","mintlify_get_assistant_conversations","mintlify_get_feedback","mintlify_get_feedback_by_page","mintlify_get_page_content","mintlify_get_searches","mintlify_get_update_status","mintlify_get_views","mintlify_get_visitors","mintlify_search","mintlify_send_agent_message","mintlify_trigger_automation","mintlify_trigger_preview","mintlify_trigger_update","mistral_parser","mistral_parser_v2","mistral_parser_v3","modal_call_function","modal_chat_completion","modal_list_models","monday_archive_item","monday_change_column_value","monday_create_board","monday_create_column","monday_create_group","monday_create_item","monday_create_subitem","monday_create_update","monday_delete_item","monday_duplicate_item","monday_get_board","monday_get_groups","monday_get_item","monday_get_items","monday_list_boards","monday_move_item_to_group","monday_search_items","monday_update_item","mongodb_delete","mongodb_execute","mongodb_insert","mongodb_introspect","mongodb_query","mongodb_update","mssql_delete","mssql_execute","mssql_insert","mssql_introspect","mssql_query","mssql_update","mysql_delete","mysql_execute","mysql_insert","mysql_introspect","mysql_query","mysql_update","neo4j_create","neo4j_delete","neo4j_execute","neo4j_introspect","neo4j_merge","neo4j_query","neo4j_update","netsuite_attach_record","netsuite_batch_create_records","netsuite_batch_delete_records","netsuite_batch_get_records","netsuite_batch_update_records","netsuite_batch_upsert_records","netsuite_create_record","netsuite_delete_record","netsuite_detach_record","netsuite_execute_action","netsuite_execute_dataset","netsuite_execute_suiteql","netsuite_get_async_result","netsuite_get_async_status","netsuite_get_governance_limits","netsuite_get_record","netsuite_get_record_form","netsuite_get_record_metadata","netsuite_get_select_options","netsuite_get_server_time","netsuite_get_subresource","netsuite_list_datasets","netsuite_list_record_types","netsuite_list_records","netsuite_transform_record","netsuite_update_record","netsuite_upsert_record","neverbounce_get_credits","neverbounce_verify_email","new_relic_create_deployment_event","new_relic_get_entity","new_relic_nrql_query","new_relic_search_entities","notion_add_database_row","notion_add_database_row_v2","notion_append_blocks","notion_append_blocks_v2","notion_create_comment","notion_create_comment_v2","notion_create_database","notion_create_database_v2","notion_create_page","notion_create_page_v2","notion_delete_block","notion_delete_block_v2","notion_list_comments","notion_list_comments_v2","notion_list_users","notion_list_users_v2","notion_query_database","notion_query_database_v2","notion_read","notion_read_database","notion_read_database_v2","notion_read_v2","notion_retrieve_block","notion_retrieve_block_children","notion_retrieve_block_children_v2","notion_retrieve_block_v2","notion_retrieve_user","notion_retrieve_user_v2","notion_search","notion_search_v2","notion_update_block","notion_update_block_v2","notion_update_page","notion_update_page_v2","notion_write","notion_write_v2","obsidian_append_active","obsidian_append_note","obsidian_append_periodic_note","obsidian_create_note","obsidian_delete_note","obsidian_execute_command","obsidian_get_active","obsidian_get_note","obsidian_get_periodic_note","obsidian_list_commands","obsidian_list_files","obsidian_open_file","obsidian_patch_active","obsidian_patch_note","obsidian_search","okta_activate_group_rule","okta_activate_user","okta_add_user_to_group","okta_assign_group_to_app","okta_assign_user_role","okta_assign_user_to_app","okta_clear_user_sessions","okta_create_group","okta_create_group_rule","okta_create_user","okta_deactivate_group_rule","okta_deactivate_user","okta_delete_group","okta_delete_group_rule","okta_delete_user","okta_enroll_factor","okta_get_app","okta_get_factor","okta_get_group","okta_get_group_rule","okta_get_logs","okta_get_session","okta_get_user","okta_list_app_groups","okta_list_app_users","okta_list_apps","okta_list_factors","okta_list_group_members","okta_list_group_rules","okta_list_groups","okta_list_user_roles","okta_list_users","okta_remove_group_from_app","okta_remove_user_from_app","okta_remove_user_from_group","okta_remove_user_role","okta_reset_all_factors","okta_reset_factor","okta_reset_password","okta_revoke_session","okta_suspend_user","okta_unsuspend_user","okta_update_group","okta_update_user","onedrive_copy","onedrive_create_folder","onedrive_create_share_link","onedrive_delete","onedrive_download","onedrive_get_drive_info","onedrive_get_item","onedrive_list","onedrive_move","onedrive_search","onedrive_upload","onepassword_create_item","onepassword_delete_item","onepassword_get_item","onepassword_get_item_file","onepassword_get_vault","onepassword_list_items","onepassword_list_vaults","onepassword_replace_item","onepassword_resolve_secret","onepassword_update_item","openai_embeddings","openai_image","outlook_calendar_create_event","outlook_calendar_delete_event","outlook_calendar_get_event","outlook_calendar_list_events","outlook_calendar_respond","outlook_calendar_update_event","outlook_copy","outlook_create_folder","outlook_delete","outlook_draft","outlook_forward","outlook_get_attachment","outlook_list_attachments","outlook_list_folders","outlook_mark_read","outlook_mark_unread","outlook_move","outlook_read","outlook_reply","outlook_reply_all","outlook_search","outlook_send","outlook_update_message","pagerduty_add_note","pagerduty_create_incident","pagerduty_get_incident","pagerduty_get_service","pagerduty_list_escalation_policies","pagerduty_list_incident_alerts","pagerduty_list_incidents","pagerduty_list_oncalls","pagerduty_list_schedules","pagerduty_list_services","pagerduty_list_users","pagerduty_merge_incidents","pagerduty_send_event","pagerduty_snooze_incident","pagerduty_update_incident","parallel_deep_research","parallel_extract","parallel_search","pdl_autocomplete","pdl_bulk_company_enrich","pdl_bulk_person_enrich","pdl_clean_company","pdl_clean_location","pdl_clean_school","pdl_company_enrich","pdl_company_search","pdl_person_enrich","pdl_person_identify","pdl_person_search","perplexity_chat","perplexity_search","persona_approve_inquiry","persona_create_account","persona_create_inquiry","persona_create_report","persona_decline_inquiry","persona_expire_inquiry","persona_generate_inquiry_link","persona_get_account","persona_get_case","persona_get_document","persona_get_inquiry","persona_get_report","persona_get_verification","persona_import_accounts","persona_list_accounts","persona_list_cases","persona_list_inquiries","persona_list_inquiry_templates","persona_list_reports","persona_mark_inquiry_for_review","persona_print_inquiry_pdf","persona_redact_account","persona_redact_inquiry","persona_resume_inquiry","persona_update_account","persona_update_inquiry","pinecone_delete_vectors","pinecone_describe_index","pinecone_describe_index_stats","pinecone_fetch","pinecone_generate_embeddings","pinecone_list_indexes","pinecone_list_vector_ids","pinecone_search_text","pinecone_search_vector","pinecone_update_vector","pinecone_upsert_text","pipedrive_create_activity","pipedrive_create_deal","pipedrive_create_lead","pipedrive_create_project","pipedrive_delete_lead","pipedrive_get_activities","pipedrive_get_all_deals","pipedrive_get_deal","pipedrive_get_files","pipedrive_get_leads","pipedrive_get_mail_messages","pipedrive_get_mail_thread","pipedrive_get_pipeline_deals","pipedrive_get_pipelines","pipedrive_get_projects","pipedrive_update_activity","pipedrive_update_deal","pipedrive_update_lead","pitchbook_company_active_investors","pitchbook_company_bio","pitchbook_company_deal_service_providers","pitchbook_company_deals","pitchbook_company_financials","pitchbook_company_general_service_providers","pitchbook_company_industries","pitchbook_company_investors","pitchbook_company_most_recent_debt_financing","pitchbook_company_most_recent_financials","pitchbook_company_most_recent_financing","pitchbook_company_search","pitchbook_company_similar_companies","pitchbook_company_social_analytics","pitchbook_company_updates","pitchbook_company_vc_exit_predictions","pitchbook_contracts_history","pitchbook_cost_of_calls","pitchbook_credit_history","pitchbook_credit_news","pitchbook_credit_news_bulk","pitchbook_credit_news_most_recent","pitchbook_credit_news_search","pitchbook_deal_bio","pitchbook_deal_cap_table_history","pitchbook_deal_debt_lenders","pitchbook_deal_detailed","pitchbook_deal_investors","pitchbook_deal_multiples","pitchbook_deal_search","pitchbook_deal_service_providers","pitchbook_deal_stock_info","pitchbook_deal_tranche_info","pitchbook_deal_updates","pitchbook_deal_valuation","pitchbook_entity_affiliates","pitchbook_entity_locations","pitchbook_entity_news","pitchbook_entity_people","pitchbook_entity_updates","pitchbook_fund_active_investments","pitchbook_fund_benchmark","pitchbook_fund_bio","pitchbook_fund_cash_flows","pitchbook_fund_commitments","pitchbook_fund_investment_preferences","pitchbook_fund_investments","pitchbook_fund_performance","pitchbook_fund_search","pitchbook_fund_team","pitchbook_fund_updates","pitchbook_investor_active_investments","pitchbook_investor_bio","pitchbook_investor_board_seats","pitchbook_investor_deal_service_providers","pitchbook_investor_funds","pitchbook_investor_general_service_providers","pitchbook_investor_investments","pitchbook_investor_last_closed_fund","pitchbook_investor_preferences","pitchbook_investor_search","pitchbook_investor_updates","pitchbook_limited_partner_actual_allocations","pitchbook_limited_partner_bio","pitchbook_limited_partner_commitment_aggregates","pitchbook_limited_partner_commitment_preferences","pitchbook_limited_partner_commitments_detailed","pitchbook_limited_partner_search","pitchbook_limited_partner_service_providers","pitchbook_limited_partner_target_allocations","pitchbook_limited_partner_updates","pitchbook_lookup_table_structure","pitchbook_lookup_tables","pitchbook_patent_detailed","pitchbook_patent_search","pitchbook_people_search","pitchbook_person_bio","pitchbook_person_contact","pitchbook_person_education_work","pitchbook_sandbox_entities","pitchbook_search","pitchbook_service_provider_bio","pitchbook_service_provider_search","pitchbook_service_provider_updates","pitchbook_serviced_companies","pitchbook_serviced_deals","pitchbook_serviced_funds","pitchbook_serviced_investors","pitchbook_serviced_limited_partners","pitchbook_shared_search","pitchbook_usage_report","polymarket_get_activity","polymarket_get_event","polymarket_get_events","polymarket_get_holders","polymarket_get_last_trade_price","polymarket_get_leaderboard","polymarket_get_market","polymarket_get_markets","polymarket_get_midpoint","polymarket_get_orderbook","polymarket_get_positions","polymarket_get_price","polymarket_get_price_history","polymarket_get_series","polymarket_get_series_by_id","polymarket_get_spread","polymarket_get_tags","polymarket_get_tick_size","polymarket_get_trades","polymarket_search","postgresql_delete","postgresql_execute","postgresql_insert","postgresql_introspect","postgresql_query","postgresql_update","posthog_batch_events","posthog_capture_event","posthog_create_annotation","posthog_create_cohort","posthog_create_dashboard","posthog_create_experiment","posthog_create_feature_flag","posthog_create_insight","posthog_create_survey","posthog_delete_feature_flag","posthog_delete_person","posthog_delete_survey","posthog_evaluate_flags","posthog_get_cohort","posthog_get_dashboard","posthog_get_event_definition","posthog_get_experiment","posthog_get_feature_flag","posthog_get_insight","posthog_get_organization","posthog_get_person","posthog_get_project","posthog_get_property_definition","posthog_get_session_recording","posthog_get_survey","posthog_list_actions","posthog_list_annotations","posthog_list_cohorts","posthog_list_dashboards","posthog_list_event_definitions","posthog_list_experiments","posthog_list_feature_flags","posthog_list_insights","posthog_list_organizations","posthog_list_persons","posthog_list_projects","posthog_list_property_definitions","posthog_list_recording_playlists","posthog_list_session_recordings","posthog_list_surveys","posthog_query","posthog_update_cohort","posthog_update_event_definition","posthog_update_experiment","posthog_update_feature_flag","posthog_update_insight","posthog_update_property_definition","posthog_update_survey","profound_bot_logs","profound_bots_report","profound_category_assets","profound_category_personas","profound_category_prompts","profound_category_tags","profound_category_topics","profound_citation_prompts","profound_citations_report","profound_list_assets","profound_list_categories","profound_list_domains","profound_list_models","profound_list_optimizations","profound_list_personas","profound_list_regions","profound_optimization_analysis","profound_prompt_answers","profound_prompt_volume","profound_query_fanouts","profound_raw_logs","profound_referrals_report","profound_sentiment_report","profound_visibility_report","prospeo_account_information","prospeo_bulk_enrich_company","prospeo_bulk_enrich_person","prospeo_enrich_company","prospeo_enrich_person","prospeo_search_company","prospeo_search_person","prospeo_search_suggestions","pulse_parser","pulse_parser_v2","qdrant_fetch_points","qdrant_search_vector","qdrant_upsert_points","quartr_get_audio","quartr_get_company","quartr_get_event","quartr_get_event_summary","quartr_get_report","quartr_get_slide_deck","quartr_get_transcript","quartr_list_audio","quartr_list_companies","quartr_list_document_types","quartr_list_documents","quartr_list_event_types","quartr_list_events","quartr_list_live_events","quartr_list_reports","quartr_list_slide_decks","quartr_list_transcripts","quiver_image_to_svg","quiver_list_models","quiver_text_to_svg","rabbitmq_create_binding","rabbitmq_create_exchange","rabbitmq_create_policy","rabbitmq_create_queue","rabbitmq_delete_binding","rabbitmq_delete_exchange","rabbitmq_delete_policy","rabbitmq_delete_queue","rabbitmq_get_exchange","rabbitmq_get_messages","rabbitmq_get_overview","rabbitmq_get_queue","rabbitmq_health_check","rabbitmq_list_bindings","rabbitmq_list_channels","rabbitmq_list_connections","rabbitmq_list_consumers","rabbitmq_list_exchange_bindings","rabbitmq_list_exchanges","rabbitmq_list_nodes","rabbitmq_list_policies","rabbitmq_list_queues","rabbitmq_list_vhosts","rabbitmq_publish_message","rabbitmq_purge_queue","railway_create_environment","railway_create_project","railway_create_service","railway_delete_environment","railway_delete_project","railway_delete_service","railway_delete_variable","railway_deploy_service","railway_get_deployment","railway_get_deployment_logs","railway_get_project","railway_list_deployments","railway_list_project_members","railway_list_projects","railway_list_variables","railway_restart_deployment","railway_rollback_deployment","railway_transfer_project","railway_update_project","railway_upsert_variable","rb2b_credit_check","rb2b_email_to_activity","rb2b_hem_to_best_linkedin","rb2b_hem_to_business_profile","rb2b_hem_to_linkedin","rb2b_hem_to_maid","rb2b_ip_to_company","rb2b_ip_to_hem","rb2b_ip_to_maid","rb2b_linkedin_slug_search","rb2b_linkedin_to_best_personal_email","rb2b_linkedin_to_business_profile","rb2b_linkedin_to_hashed_emails","rb2b_linkedin_to_mobile_phone","rb2b_linkedin_to_personal_email","rds_delete","rds_execute","rds_insert","rds_introspect","rds_query","rds_update","reddit_delete","reddit_edit","reddit_get_comments","reddit_get_controversial","reddit_get_info","reddit_get_me","reddit_get_messages","reddit_get_posts","reddit_get_saved","reddit_get_subreddit_info","reddit_get_subreddit_rules","reddit_get_user","reddit_get_user_comments","reddit_get_user_posts","reddit_hide","reddit_hot_posts","reddit_list_my_subreddits","reddit_lock","reddit_mark_all_read","reddit_mark_read","reddit_marknsfw","reddit_mod_approve","reddit_mod_distinguish","reddit_mod_remove","reddit_mod_sticky","reddit_reply","reddit_report","reddit_save","reddit_search","reddit_search_subreddits","reddit_send_message","reddit_submit_post","reddit_subscribe","reddit_unhide","reddit_unlock","reddit_unmarknsfw","reddit_unsave","reddit_vote","redis_command","redis_delete","redis_exists","redis_expire","redis_get","redis_hdel","redis_hget","redis_hgetall","redis_hset","redis_incr","redis_incrby","redis_keys","redis_llen","redis_lpop","redis_lpush","redis_lrange","redis_persist","redis_rpop","redis_rpush","redis_set","redis_setnx","redis_ttl","reducto_parser","reducto_parser_v2","resend_cancel_email","resend_create_audience","resend_create_broadcast","resend_create_contact","resend_delete_audience","resend_delete_contact","resend_get_audience","resend_get_broadcast","resend_get_contact","resend_get_email","resend_list_audiences","resend_list_contacts","resend_list_domains","resend_send","resend_send_broadcast","resend_update_contact","revenuecat_create_purchase","revenuecat_defer_google_subscription","revenuecat_delete_customer","revenuecat_get_customer","revenuecat_grant_entitlement","revenuecat_list_offerings","revenuecat_refund_google_subscription","revenuecat_revoke_entitlement","revenuecat_revoke_google_subscription","revenuecat_update_subscriber_attributes","rippling_bulk_create_custom_object_records","rippling_bulk_delete_custom_object_records","rippling_bulk_update_custom_object_records","rippling_create_business_partner","rippling_create_business_partner_group","rippling_create_custom_app","rippling_create_custom_object","rippling_create_custom_object_field","rippling_create_custom_object_record","rippling_create_custom_page","rippling_create_custom_setting","rippling_create_department","rippling_create_draft_hires","rippling_create_object_category","rippling_create_title","rippling_create_work_location","rippling_delete_business_partner","rippling_delete_business_partner_group","rippling_delete_custom_app","rippling_delete_custom_object","rippling_delete_custom_object_field","rippling_delete_custom_object_record","rippling_delete_custom_page","rippling_delete_custom_setting","rippling_delete_object_category","rippling_delete_title","rippling_delete_work_location","rippling_get_business_partner","rippling_get_business_partner_group","rippling_get_current_user","rippling_get_custom_app","rippling_get_custom_object","rippling_get_custom_object_field","rippling_get_custom_object_record","rippling_get_custom_object_record_by_external_id","rippling_get_custom_page","rippling_get_custom_setting","rippling_get_department","rippling_get_employment_type","rippling_get_job_function","rippling_get_object_category","rippling_get_report_run","rippling_get_supergroup","rippling_get_team","rippling_get_title","rippling_get_user","rippling_get_work_location","rippling_get_worker","rippling_list_business_partner_groups","rippling_list_business_partners","rippling_list_companies","rippling_list_custom_apps","rippling_list_custom_fields","rippling_list_custom_object_fields","rippling_list_custom_object_records","rippling_list_custom_objects","rippling_list_custom_pages","rippling_list_custom_settings","rippling_list_departments","rippling_list_employment_types","rippling_list_entitlements","rippling_list_job_functions","rippling_list_object_categories","rippling_list_supergroup_exclusion_members","rippling_list_supergroup_inclusion_members","rippling_list_supergroup_members","rippling_list_supergroups","rippling_list_teams","rippling_list_titles","rippling_list_users","rippling_list_work_locations","rippling_list_workers","rippling_query_custom_object_records","rippling_trigger_report_run","rippling_update_custom_app","rippling_update_custom_object","rippling_update_custom_object_field","rippling_update_custom_object_record","rippling_update_custom_page","rippling_update_custom_setting","rippling_update_department","rippling_update_object_category","rippling_update_supergroup_exclusion_members","rippling_update_supergroup_inclusion_members","rippling_update_title","rippling_update_work_location","rocketlane_add_field_option","rocketlane_add_project_members","rocketlane_add_task_assignees","rocketlane_add_task_dependencies","rocketlane_add_task_followers","rocketlane_archive_project","rocketlane_assign_placeholders","rocketlane_create_field","rocketlane_create_phase","rocketlane_create_project","rocketlane_create_space","rocketlane_create_space_document","rocketlane_create_task","rocketlane_create_time_entry","rocketlane_create_time_off","rocketlane_delete_field","rocketlane_delete_phase","rocketlane_delete_project","rocketlane_delete_space","rocketlane_delete_space_document","rocketlane_delete_task","rocketlane_delete_time_entry","rocketlane_delete_time_off","rocketlane_get_field","rocketlane_get_invoice","rocketlane_get_invoice_line_items","rocketlane_get_invoice_payments","rocketlane_get_phase","rocketlane_get_project","rocketlane_get_space","rocketlane_get_space_document","rocketlane_get_task","rocketlane_get_time_entry","rocketlane_get_time_off","rocketlane_get_user","rocketlane_import_template","rocketlane_list_fields","rocketlane_list_invoices","rocketlane_list_phases","rocketlane_list_placeholders","rocketlane_list_projects","rocketlane_list_resource_allocations","rocketlane_list_space_documents","rocketlane_list_spaces","rocketlane_list_tasks","rocketlane_list_time_entries","rocketlane_list_time_entry_categories","rocketlane_list_time_offs","rocketlane_list_users","rocketlane_move_task_to_phase","rocketlane_remove_project_members","rocketlane_remove_task_assignees","rocketlane_remove_task_dependencies","rocketlane_remove_task_followers","rocketlane_search_time_entries","rocketlane_unassign_placeholders","rocketlane_update_field","rocketlane_update_field_option","rocketlane_update_phase","rocketlane_update_project","rocketlane_update_space","rocketlane_update_space_document","rocketlane_update_task","rocketlane_update_time_entry","rootly_acknowledge_alert","rootly_add_incident_event","rootly_add_subscribers","rootly_assign_incident_role","rootly_create_action_item","rootly_create_alert","rootly_create_incident","rootly_create_status_page_event","rootly_delete_action_item","rootly_delete_incident","rootly_escalate_alert","rootly_get_alert","rootly_get_incident","rootly_list_action_items","rootly_list_alerts","rootly_list_causes","rootly_list_environments","rootly_list_escalation_policies","rootly_list_functionalities","rootly_list_incident_events","rootly_list_incident_roles","rootly_list_incident_types","rootly_list_incidents","rootly_list_on_calls","rootly_list_playbooks","rootly_list_retrospectives","rootly_list_schedules","rootly_list_services","rootly_list_severities","rootly_list_teams","rootly_list_users","rootly_mitigate_incident","rootly_remove_subscribers","rootly_resolve_alert","rootly_resolve_incident","rootly_run_workflow","rootly_snooze_alert","rootly_unassign_incident_role","rootly_update_action_item","rootly_update_alert","rootly_update_incident","s3_copy_object","s3_create_bucket","s3_delete_bucket","s3_delete_object","s3_delete_objects","s3_get_object","s3_head_object","s3_list_buckets","s3_list_objects","s3_presigned_url","s3_put_object","sailpoint_approve_access_request","sailpoint_cancel_access_request","sailpoint_decide_certification_review_items","sailpoint_get_access_profile","sailpoint_get_access_profile_entitlements","sailpoint_get_access_request_config","sailpoint_get_access_request_status","sailpoint_get_account","sailpoint_get_account_activity","sailpoint_get_account_entitlements","sailpoint_get_account_selections","sailpoint_get_campaign","sailpoint_get_certification","sailpoint_get_entitlement","sailpoint_get_entitlement_request_config","sailpoint_get_identity","sailpoint_get_role","sailpoint_get_role_entitlements","sailpoint_get_source","sailpoint_get_task_status","sailpoint_list_access_profiles","sailpoint_list_account_activities","sailpoint_list_accounts","sailpoint_list_campaigns","sailpoint_list_certification_review_items","sailpoint_list_certifications","sailpoint_list_entitlements","sailpoint_list_identities","sailpoint_list_identity_entitlements","sailpoint_list_pending_access_request_approvals","sailpoint_list_roles","sailpoint_list_sources","sailpoint_load_accounts","sailpoint_load_entitlements","sailpoint_reject_access_request","sailpoint_request_access","sailpoint_search","sailpoint_search_aggregate","sailpoint_search_count","sailpoint_sign_off_certification","salesforce_create_account","salesforce_create_case","salesforce_create_contact","salesforce_create_custom_field","salesforce_create_custom_object","salesforce_create_lead","salesforce_create_opportunity","salesforce_create_task","salesforce_delete_account","salesforce_delete_case","salesforce_delete_contact","salesforce_delete_custom_field","salesforce_delete_lead","salesforce_delete_opportunity","salesforce_delete_task","salesforce_describe_object","salesforce_get_accounts","salesforce_get_cases","salesforce_get_contacts","salesforce_get_dashboard","salesforce_get_leads","salesforce_get_opportunities","salesforce_get_report","salesforce_get_tasks","salesforce_list_dashboards","salesforce_list_objects","salesforce_list_report_types","salesforce_list_reports","salesforce_query","salesforce_query_more","salesforce_refresh_dashboard","salesforce_run_report","salesforce_tooling_query","salesforce_update_account","salesforce_update_case","salesforce_update_contact","salesforce_update_custom_field","salesforce_update_lead","salesforce_update_opportunity","salesforce_update_task","sap_concur_approve_expense_report","sap_concur_associate_attendees","sap_concur_create_cash_advance","sap_concur_create_expected_expense","sap_concur_create_expense_report","sap_concur_create_list_item","sap_concur_create_purchase_request","sap_concur_create_quick_expense","sap_concur_create_quick_expense_with_image","sap_concur_create_report_comment","sap_concur_create_travel_request","sap_concur_create_user","sap_concur_delete_expected_expense","sap_concur_delete_expense","sap_concur_delete_expense_report","sap_concur_delete_list_item","sap_concur_delete_travel_request","sap_concur_delete_user","sap_concur_get_allocation","sap_concur_get_budget","sap_concur_get_cash_advance","sap_concur_get_expected_expense","sap_concur_get_expense","sap_concur_get_expense_report","sap_concur_get_itemizations","sap_concur_get_itinerary","sap_concur_get_list","sap_concur_get_list_item","sap_concur_get_purchase_request","sap_concur_get_receipt","sap_concur_get_receipt_status","sap_concur_get_request_cash_advance","sap_concur_get_travel_profile","sap_concur_get_travel_request","sap_concur_get_user","sap_concur_issue_cash_advance","sap_concur_list_allocations","sap_concur_list_attendee_associations","sap_concur_list_budget_categories","sap_concur_list_budgets","sap_concur_list_exceptions","sap_concur_list_expected_expenses","sap_concur_list_expense_reports","sap_concur_list_expenses","sap_concur_list_itineraries","sap_concur_list_list_items","sap_concur_list_lists","sap_concur_list_receipts","sap_concur_list_report_comments","sap_concur_list_reports_to_approve","sap_concur_list_travel_profiles_summary","sap_concur_list_travel_request_comments","sap_concur_list_travel_requests","sap_concur_list_users","sap_concur_move_travel_request","sap_concur_recall_expense_report","sap_concur_remove_all_attendees","sap_concur_search_locations","sap_concur_search_users","sap_concur_send_back_expense_report","sap_concur_submit_expense_report","sap_concur_update_allocation","sap_concur_update_expected_expense","sap_concur_update_expense","sap_concur_update_expense_report","sap_concur_update_list_item","sap_concur_update_travel_request","sap_concur_update_user","sap_concur_upload_exchange_rates","sap_concur_upload_receipt_image","sap_s4hana_create_business_partner","sap_s4hana_create_purchase_order","sap_s4hana_create_purchase_requisition","sap_s4hana_create_sales_order","sap_s4hana_delete_sales_order","sap_s4hana_get_billing_document","sap_s4hana_get_business_partner","sap_s4hana_get_customer","sap_s4hana_get_inbound_delivery","sap_s4hana_get_material_document","sap_s4hana_get_outbound_delivery","sap_s4hana_get_product","sap_s4hana_get_purchase_order","sap_s4hana_get_purchase_requisition","sap_s4hana_get_sales_order","sap_s4hana_get_supplier","sap_s4hana_get_supplier_invoice","sap_s4hana_list_billing_documents","sap_s4hana_list_business_partners","sap_s4hana_list_customers","sap_s4hana_list_inbound_deliveries","sap_s4hana_list_material_documents","sap_s4hana_list_material_stock","sap_s4hana_list_outbound_deliveries","sap_s4hana_list_products","sap_s4hana_list_purchase_orders","sap_s4hana_list_purchase_requisitions","sap_s4hana_list_sales_orders","sap_s4hana_list_supplier_invoices","sap_s4hana_list_suppliers","sap_s4hana_odata_query","sap_s4hana_update_business_partner","sap_s4hana_update_customer","sap_s4hana_update_product","sap_s4hana_update_purchase_order","sap_s4hana_update_purchase_requisition","sap_s4hana_update_sales_order","sap_s4hana_update_supplier","search_tool","secrets_manager_create_secret","secrets_manager_delete_secret","secrets_manager_describe_secret","secrets_manager_get_secret","secrets_manager_list_secrets","secrets_manager_restore_secret","secrets_manager_rotate_secret","secrets_manager_tag_resource","secrets_manager_untag_resource","secrets_manager_update_secret","semrush_backlinks","semrush_backlinks_anchors","semrush_backlinks_competitors","semrush_backlinks_geo_distribution","semrush_backlinks_indexed_pages","semrush_backlinks_overview","semrush_backlinks_tld_distribution","semrush_batch_keyword_overview","semrush_broad_match_keywords","semrush_domain_ad_copies","semrush_domain_ad_history","semrush_domain_organic_competitors","semrush_domain_organic_keywords","semrush_domain_overview","semrush_domain_overview_all","semrush_domain_overview_history","semrush_domain_paid_competitors","semrush_domain_paid_keywords","semrush_domain_pla_copies","semrush_domain_pla_keywords","semrush_domain_vs_domain","semrush_keyword_ad_history","semrush_keyword_difficulty","semrush_keyword_overview","semrush_keyword_overview_all","semrush_keyword_questions","semrush_organic_results","semrush_paid_results","semrush_referring_domains","semrush_referring_ips","semrush_related_keywords","semrush_subdomain_ad_copies","semrush_subdomain_organic_keywords","semrush_subdomain_overview","semrush_subdomain_overview_all","semrush_subdomain_overview_history","semrush_subdomain_paid_keywords","semrush_top_domains","semrush_url_organic_keywords","semrush_url_overview","semrush_url_overview_all","semrush_url_overview_history","semrush_url_paid_keywords","semrush_winners_and_losers","sendblue_evaluate_service","sendblue_get_message","sendblue_send_group_message","sendblue_send_message","sendblue_send_typing_indicator","sendgrid_add_contact","sendgrid_add_contacts_to_list","sendgrid_create_list","sendgrid_create_template","sendgrid_create_template_version","sendgrid_delete_contacts","sendgrid_delete_list","sendgrid_delete_template","sendgrid_get_contact","sendgrid_get_list","sendgrid_get_template","sendgrid_list_all_lists","sendgrid_list_templates","sendgrid_remove_contacts_from_list","sendgrid_search_contacts","sendgrid_send_mail","sentry_events_get","sentry_events_list","sentry_issues_get","sentry_issues_list","sentry_issues_update","sentry_projects_create","sentry_projects_get","sentry_projects_list","sentry_projects_update","sentry_releases_create","sentry_releases_deploy","sentry_releases_list","sentry_teams_list","serper_search","servicenow_add_incident_comment","servicenow_aggregate","servicenow_close_incident","servicenow_create_change_request","servicenow_create_incident","servicenow_create_record","servicenow_delete_record","servicenow_download_attachment","servicenow_find_user","servicenow_get_change_next_states","servicenow_get_change_request","servicenow_get_ci","servicenow_get_incident","servicenow_get_knowledge_article","servicenow_get_requested_item","servicenow_list_approvals","servicenow_list_attachments","servicenow_list_catalog_items","servicenow_list_change_requests","servicenow_list_change_tasks","servicenow_list_ci_relationships","servicenow_list_group_members","servicenow_list_incidents","servicenow_list_requested_items","servicenow_order_catalog_item","servicenow_read_record","servicenow_resolve_incident","servicenow_search_cis","servicenow_search_knowledge","servicenow_update_approval","servicenow_update_change_request","servicenow_update_change_state","servicenow_update_incident","servicenow_update_record","servicenow_upload_attachment","ses_create_configuration_set","ses_create_email_identity","ses_create_template","ses_delete_email_identity","ses_delete_suppressed_destination","ses_delete_template","ses_get_account","ses_get_email_identity","ses_get_suppressed_destination","ses_get_template","ses_list_identities","ses_list_suppressed_destinations","ses_list_templates","ses_put_suppressed_destination","ses_send_bulk_email","ses_send_custom_verification_email","ses_send_email","ses_send_templated_email","ses_update_template","sftp_delete","sftp_download","sftp_list","sftp_mkdir","sftp_upload","sharepoint_add_list_items","sharepoint_create_list","sharepoint_create_page","sharepoint_delete_file","sharepoint_delete_list_item","sharepoint_delete_page","sharepoint_download_file","sharepoint_get_drive_item","sharepoint_get_list","sharepoint_get_list_item","sharepoint_list_sites","sharepoint_publish_page","sharepoint_read_page","sharepoint_update_list","sharepoint_update_page","sharepoint_upload_file","shopify_adjust_inventory","shopify_cancel_order","shopify_create_customer","shopify_create_fulfillment","shopify_create_product","shopify_delete_customer","shopify_delete_product","shopify_get_collection","shopify_get_customer","shopify_get_inventory_level","shopify_get_order","shopify_get_product","shopify_list_collections","shopify_list_customers","shopify_list_inventory_items","shopify_list_locations","shopify_list_orders","shopify_list_products","shopify_update_customer","shopify_update_order","shopify_update_product","similarweb_bounce_rate","similarweb_page_views","similarweb_pages_per_visit","similarweb_traffic_visits","similarweb_visit_duration","similarweb_website_overview","sixtyfour_enrich_company","sixtyfour_enrich_lead","sixtyfour_find_email","sixtyfour_find_phone","slack_add_reaction","slack_archive_conversation","slack_canvas","slack_create_channel_canvas","slack_create_conversation","slack_delete_canvas","slack_delete_message","slack_delete_scheduled_message","slack_download","slack_edit_canvas","slack_ephemeral_message","slack_get_canvas","slack_get_channel_history","slack_get_channel_info","slack_get_message","slack_get_permalink","slack_get_thread","slack_get_thread_replies","slack_get_user","slack_get_user_presence","slack_invite_to_conversation","slack_list_canvases","slack_list_channels","slack_list_members","slack_list_scheduled_messages","slack_list_users","slack_lookup_canvas_sections","slack_message","slack_message_reader","slack_open_view","slack_publish_view","slack_push_view","slack_remove_reaction","slack_rename_agent_session_v2","slack_rename_conversation","slack_schedule_message","slack_set_agent_session_status_v2","slack_set_conversation_purpose","slack_set_conversation_topic","slack_set_status","slack_set_suggested_prompts","slack_set_suggested_prompts_v2","slack_set_title","slack_update_message","slack_update_view","smartlead_add_email_accounts_to_campaign","smartlead_add_leads_to_campaign","smartlead_create_campaign","smartlead_create_lead_list","smartlead_delete_campaign","smartlead_delete_campaign_webhook","smartlead_delete_lead_from_campaign","smartlead_delete_lead_list","smartlead_duplicate_campaign","smartlead_export_campaign_leads","smartlead_get_campaign","smartlead_get_campaign_analytics","smartlead_get_campaign_analytics_by_date","smartlead_get_campaign_lead_statistics","smartlead_get_campaign_mailbox_statistics","smartlead_get_campaign_sequences","smartlead_get_campaign_statistics","smartlead_get_campaign_top_level_analytics_by_date","smartlead_get_campaign_webhook_summary","smartlead_get_lead_by_email","smartlead_get_lead_by_id","smartlead_get_lead_list","smartlead_get_lead_message_history","smartlead_list_campaign_email_accounts","smartlead_list_campaign_leads","smartlead_list_campaign_webhooks","smartlead_list_campaigns","smartlead_list_clients","smartlead_list_email_accounts","smartlead_list_inbox_replies","smartlead_list_lead_activities","smartlead_list_lead_categories","smartlead_list_lead_lists","smartlead_mark_lead_complete","smartlead_pause_lead","smartlead_remove_email_accounts_from_campaign","smartlead_resume_lead","smartlead_save_campaign_sequences","smartlead_unsubscribe_lead_from_campaign","smartlead_unsubscribe_lead_globally","smartlead_update_campaign_schedule","smartlead_update_campaign_settings","smartlead_update_campaign_status","smartlead_update_lead","smartlead_update_lead_category","smartlead_update_lead_list","smartlead_upsert_campaign_webhook","sms_send","smtp_send_mail","snowflake_alter_warehouse","snowflake_call_procedure","snowflake_cancel_statement","snowflake_cancel_task_run","snowflake_delete_rows","snowflake_execute_sql","snowflake_get_statement","snowflake_get_task","snowflake_get_task_run","snowflake_get_task_run_output","snowflake_get_warehouse","snowflake_insert_rows","snowflake_introspect_schema","snowflake_list_copy_history","snowflake_list_databases","snowflake_list_query_history","snowflake_list_schemas","snowflake_list_tables","snowflake_list_task_runs","snowflake_list_tasks","snowflake_list_warehouses","snowflake_load_data","snowflake_resume_task","snowflake_resume_warehouse","snowflake_run_task","snowflake_suspend_task","snowflake_suspend_warehouse","snowflake_unload_data","snowflake_update_rows","snowflake_upsert_rows","splunk_cancel_search_job","splunk_create_search_job","splunk_dispatch_saved_search","splunk_get_fired_alerts","splunk_get_saved_search","splunk_get_search_job","splunk_get_search_results","splunk_list_apps","splunk_list_fired_alerts","splunk_list_indexes","splunk_list_saved_searches","splunk_run_search","sportmonks_core_get_cities","sportmonks_core_get_city","sportmonks_core_get_continent","sportmonks_core_get_continents","sportmonks_core_get_countries","sportmonks_core_get_country","sportmonks_core_get_entity_filters","sportmonks_core_get_my_usage","sportmonks_core_get_region","sportmonks_core_get_regions","sportmonks_core_get_timezones","sportmonks_core_get_type","sportmonks_core_get_type_by_entity","sportmonks_core_get_types","sportmonks_core_search_cities","sportmonks_core_search_countries","sportmonks_core_search_regions","sportmonks_football_expected_by_player","sportmonks_football_expected_by_team","sportmonks_football_get_all_commentaries","sportmonks_football_get_all_fixtures","sportmonks_football_get_all_players","sportmonks_football_get_all_rivals","sportmonks_football_get_all_teams","sportmonks_football_get_all_transfer_rumours","sportmonks_football_get_all_transfers","sportmonks_football_get_brackets_by_season","sportmonks_football_get_coach","sportmonks_football_get_coaches","sportmonks_football_get_coaches_by_country","sportmonks_football_get_commentaries_by_fixture","sportmonks_football_get_current_leagues_by_team","sportmonks_football_get_expected_lineups_by_player","sportmonks_football_get_expected_lineups_by_team","sportmonks_football_get_extended_team_squad","sportmonks_football_get_fixture","sportmonks_football_get_fixtures_by_date","sportmonks_football_get_fixtures_by_date_range","sportmonks_football_get_fixtures_by_date_range_for_team","sportmonks_football_get_fixtures_by_ids","sportmonks_football_get_grouped_standings_by_round","sportmonks_football_get_head_to_head","sportmonks_football_get_inplay_livescores","sportmonks_football_get_latest_coaches","sportmonks_football_get_latest_fixtures","sportmonks_football_get_latest_livescores","sportmonks_football_get_latest_players","sportmonks_football_get_latest_totw","sportmonks_football_get_latest_transfers","sportmonks_football_get_league","sportmonks_football_get_leagues","sportmonks_football_get_leagues_by_country","sportmonks_football_get_leagues_by_date","sportmonks_football_get_leagues_by_team","sportmonks_football_get_live_leagues","sportmonks_football_get_live_probabilities","sportmonks_football_get_live_probabilities_by_fixture","sportmonks_football_get_live_standings_by_league","sportmonks_football_get_livescores","sportmonks_football_get_match_facts","sportmonks_football_get_match_facts_by_date_range","sportmonks_football_get_match_facts_by_fixture","sportmonks_football_get_match_facts_by_league","sportmonks_football_get_past_fixtures_by_tv_station","sportmonks_football_get_player","sportmonks_football_get_players_by_country","sportmonks_football_get_postmatch_news","sportmonks_football_get_postmatch_news_by_season","sportmonks_football_get_predictability_by_league","sportmonks_football_get_prematch_news","sportmonks_football_get_prematch_news_by_season","sportmonks_football_get_prematch_news_upcoming","sportmonks_football_get_probabilities","sportmonks_football_get_probabilities_by_fixture","sportmonks_football_get_referee","sportmonks_football_get_referees","sportmonks_football_get_referees_by_country","sportmonks_football_get_referees_by_season","sportmonks_football_get_rivals_by_team","sportmonks_football_get_round","sportmonks_football_get_round_statistics","sportmonks_football_get_rounds","sportmonks_football_get_rounds_by_season","sportmonks_football_get_schedules_by_season","sportmonks_football_get_schedules_by_season_and_team","sportmonks_football_get_schedules_by_team","sportmonks_football_get_season","sportmonks_football_get_seasons","sportmonks_football_get_seasons_by_team","sportmonks_football_get_stage","sportmonks_football_get_stage_statistics","sportmonks_football_get_stages","sportmonks_football_get_stages_by_season","sportmonks_football_get_standing_corrections_by_season","sportmonks_football_get_standings","sportmonks_football_get_standings_by_round","sportmonks_football_get_standings_by_season","sportmonks_football_get_state","sportmonks_football_get_states","sportmonks_football_get_team","sportmonks_football_get_team_rankings","sportmonks_football_get_team_rankings_by_date","sportmonks_football_get_team_rankings_by_team","sportmonks_football_get_team_squad","sportmonks_football_get_team_squad_by_season","sportmonks_football_get_teams_by_country","sportmonks_football_get_teams_by_season","sportmonks_football_get_topscorers_by_season","sportmonks_football_get_topscorers_by_stage","sportmonks_football_get_totw","sportmonks_football_get_totw_by_round","sportmonks_football_get_transfer","sportmonks_football_get_transfer_rumour","sportmonks_football_get_transfer_rumours_between_dates","sportmonks_football_get_transfer_rumours_by_player","sportmonks_football_get_transfer_rumours_by_team","sportmonks_football_get_transfers_between_dates","sportmonks_football_get_transfers_by_player","sportmonks_football_get_transfers_by_team","sportmonks_football_get_tv_station","sportmonks_football_get_tv_stations","sportmonks_football_get_tv_stations_by_fixture","sportmonks_football_get_upcoming_fixtures_by_market","sportmonks_football_get_upcoming_fixtures_by_tv_station","sportmonks_football_get_value_bets","sportmonks_football_get_value_bets_by_fixture","sportmonks_football_get_venue","sportmonks_football_get_venues","sportmonks_football_get_venues_by_season","sportmonks_football_search_coaches","sportmonks_football_search_fixtures","sportmonks_football_search_leagues","sportmonks_football_search_players","sportmonks_football_search_referees","sportmonks_football_search_rounds","sportmonks_football_search_seasons","sportmonks_football_search_stages","sportmonks_football_search_teams","sportmonks_football_search_venues","sportmonks_motorsport_get_all_fixtures","sportmonks_motorsport_get_current_leagues_by_team","sportmonks_motorsport_get_driver","sportmonks_motorsport_get_driver_standings","sportmonks_motorsport_get_driver_standings_by_season","sportmonks_motorsport_get_drivers","sportmonks_motorsport_get_drivers_by_country","sportmonks_motorsport_get_drivers_by_season","sportmonks_motorsport_get_fixture","sportmonks_motorsport_get_fixtures_by_date","sportmonks_motorsport_get_fixtures_by_date_range","sportmonks_motorsport_get_fixtures_by_ids","sportmonks_motorsport_get_laps_by_fixture","sportmonks_motorsport_get_laps_by_fixture_and_driver","sportmonks_motorsport_get_laps_by_fixture_and_lap","sportmonks_motorsport_get_latest_laps_by_fixture","sportmonks_motorsport_get_latest_pitstops_by_fixture","sportmonks_motorsport_get_latest_stints_by_fixture","sportmonks_motorsport_get_latest_updated_drivers","sportmonks_motorsport_get_latest_updated_fixtures","sportmonks_motorsport_get_league","sportmonks_motorsport_get_leagues","sportmonks_motorsport_get_leagues_by_country","sportmonks_motorsport_get_leagues_by_date","sportmonks_motorsport_get_leagues_by_live","sportmonks_motorsport_get_leagues_by_team","sportmonks_motorsport_get_livescores","sportmonks_motorsport_get_pitstops_by_fixture","sportmonks_motorsport_get_pitstops_by_fixture_and_driver","sportmonks_motorsport_get_pitstops_by_fixture_and_lap","sportmonks_motorsport_get_race_results_by_season_and_driver","sportmonks_motorsport_get_race_results_by_season_and_team","sportmonks_motorsport_get_schedules_by_season","sportmonks_motorsport_get_season","sportmonks_motorsport_get_seasons","sportmonks_motorsport_get_stage","sportmonks_motorsport_get_stages","sportmonks_motorsport_get_stages_by_season","sportmonks_motorsport_get_state","sportmonks_motorsport_get_states","sportmonks_motorsport_get_stints_by_fixture","sportmonks_motorsport_get_stints_by_fixture_and_driver","sportmonks_motorsport_get_stints_by_fixture_and_stint","sportmonks_motorsport_get_team","sportmonks_motorsport_get_team_standings","sportmonks_motorsport_get_team_standings_by_season","sportmonks_motorsport_get_teams","sportmonks_motorsport_get_teams_by_country","sportmonks_motorsport_get_teams_by_season","sportmonks_motorsport_get_venue","sportmonks_motorsport_get_venues","sportmonks_motorsport_get_venues_by_season","sportmonks_motorsport_search_drivers","sportmonks_motorsport_search_leagues","sportmonks_motorsport_search_stages","sportmonks_motorsport_search_teams","sportmonks_motorsport_search_venues","sportmonks_odds_get_all_historical_odds","sportmonks_odds_get_all_inplay_odds","sportmonks_odds_get_all_pre_match_odds","sportmonks_odds_get_all_premium_odds","sportmonks_odds_get_bookmaker","sportmonks_odds_get_bookmaker_event_ids_by_fixture","sportmonks_odds_get_bookmakers","sportmonks_odds_get_bookmakers_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker","sportmonks_odds_get_inplay_odds_by_fixture_and_market","sportmonks_odds_get_last_updated_inplay_odds","sportmonks_odds_get_last_updated_pre_match_odds","sportmonks_odds_get_market","sportmonks_odds_get_markets","sportmonks_odds_get_pre_match_odds_by_fixture","sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker","sportmonks_odds_get_pre_match_odds_by_fixture_and_market","sportmonks_odds_get_premium_odds_by_fixture","sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker","sportmonks_odds_get_premium_odds_by_fixture_and_market","sportmonks_odds_get_updated_historical_odds_between","sportmonks_odds_get_updated_premium_odds_between","sportmonks_odds_search_bookmakers","sportmonks_odds_search_markets","spotify_add_playlist_cover","spotify_add_to_queue","spotify_add_tracks_to_playlist","spotify_check_following","spotify_check_playlist_followers","spotify_check_saved_albums","spotify_check_saved_audiobooks","spotify_check_saved_episodes","spotify_check_saved_shows","spotify_check_saved_tracks","spotify_create_playlist","spotify_follow_artists","spotify_follow_playlist","spotify_get_album","spotify_get_album_tracks","spotify_get_albums","spotify_get_artist","spotify_get_artist_albums","spotify_get_artist_top_tracks","spotify_get_artists","spotify_get_audiobook","spotify_get_audiobook_chapters","spotify_get_audiobooks","spotify_get_categories","spotify_get_current_user","spotify_get_currently_playing","spotify_get_devices","spotify_get_episode","spotify_get_episodes","spotify_get_followed_artists","spotify_get_markets","spotify_get_new_releases","spotify_get_playback_state","spotify_get_playlist","spotify_get_playlist_cover","spotify_get_playlist_tracks","spotify_get_queue","spotify_get_recently_played","spotify_get_saved_albums","spotify_get_saved_audiobooks","spotify_get_saved_episodes","spotify_get_saved_shows","spotify_get_saved_tracks","spotify_get_show","spotify_get_show_episodes","spotify_get_shows","spotify_get_top_artists","spotify_get_top_tracks","spotify_get_track","spotify_get_tracks","spotify_get_user_playlists","spotify_get_user_profile","spotify_pause","spotify_play","spotify_remove_saved_albums","spotify_remove_saved_audiobooks","spotify_remove_saved_episodes","spotify_remove_saved_shows","spotify_remove_saved_tracks","spotify_remove_tracks_from_playlist","spotify_reorder_playlist_items","spotify_replace_playlist_items","spotify_save_albums","spotify_save_audiobooks","spotify_save_episodes","spotify_save_shows","spotify_save_tracks","spotify_search","spotify_seek","spotify_set_repeat","spotify_set_shuffle","spotify_set_volume","spotify_skip_next","spotify_skip_previous","spotify_transfer_playback","spotify_unfollow_artists","spotify_unfollow_playlist","spotify_update_playlist","sqs_send","square_batch_retrieve_inventory_counts","square_cancel_invoice","square_cancel_payment","square_complete_payment","square_create_catalog_image","square_create_customer","square_create_invoice","square_create_order","square_create_payment","square_delete_catalog_object","square_delete_customer","square_delete_invoice","square_get_catalog_object","square_get_customer","square_get_invoice","square_get_location","square_get_order","square_get_payment","square_get_refund","square_list_catalog","square_list_customers","square_list_invoices","square_list_locations","square_list_payments","square_list_refunds","square_pay_order","square_publish_invoice","square_refund_payment","square_search_catalog_objects","square_search_customers","square_search_invoices","square_search_orders","square_update_customer","square_upsert_catalog_object","ssh_check_command_exists","ssh_check_file_exists","ssh_create_directory","ssh_delete_file","ssh_download_file","ssh_execute_command","ssh_execute_script","ssh_get_system_info","ssh_list_directory","ssh_move_rename","ssh_read_file_content","ssh_upload_file","ssh_write_file_content","stagehand_agent","stagehand_extract","stripe_cancel_payment_intent","stripe_cancel_subscription","stripe_capture_charge","stripe_capture_payment_intent","stripe_confirm_payment_intent","stripe_create_charge","stripe_create_customer","stripe_create_invoice","stripe_create_payment_intent","stripe_create_price","stripe_create_product","stripe_create_subscription","stripe_delete_customer","stripe_delete_invoice","stripe_delete_product","stripe_finalize_invoice","stripe_list_charges","stripe_list_customers","stripe_list_events","stripe_list_invoices","stripe_list_payment_intents","stripe_list_prices","stripe_list_products","stripe_list_subscriptions","stripe_pay_invoice","stripe_resume_subscription","stripe_retrieve_charge","stripe_retrieve_customer","stripe_retrieve_event","stripe_retrieve_invoice","stripe_retrieve_payment_intent","stripe_retrieve_price","stripe_retrieve_product","stripe_retrieve_subscription","stripe_search_charges","stripe_search_customers","stripe_search_invoices","stripe_search_payment_intents","stripe_search_prices","stripe_search_products","stripe_search_subscriptions","stripe_send_invoice","stripe_update_charge","stripe_update_customer","stripe_update_invoice","stripe_update_payment_intent","stripe_update_price","stripe_update_product","stripe_update_subscription","stripe_void_invoice","sts_assume_role","sts_assume_role_with_saml","sts_assume_role_with_web_identity","sts_get_access_key_info","sts_get_caller_identity","sts_get_session_token","stt_assemblyai","stt_assemblyai_v2","stt_deepgram","stt_deepgram_v2","stt_elevenlabs","stt_elevenlabs_v2","stt_gemini","stt_gemini_v2","stt_whisper","stt_whisper_v2","supabase_count","supabase_delete","supabase_get_row","supabase_insert","supabase_introspect","supabase_invoke_function","supabase_query","supabase_rpc","supabase_storage_copy","supabase_storage_create_bucket","supabase_storage_create_signed_upload_url","supabase_storage_create_signed_url","supabase_storage_delete","supabase_storage_delete_bucket","supabase_storage_download","supabase_storage_empty_bucket","supabase_storage_get_public_url","supabase_storage_list","supabase_storage_list_buckets","supabase_storage_move","supabase_storage_update_bucket","supabase_storage_upload","supabase_text_search","supabase_update","supabase_upsert","supabase_vector_search","table_batch_insert_rows","table_create","table_delete_row","table_delete_rows_by_filter","table_get_row","table_get_schema","table_insert_row","table_list","table_query_rows","table_query_rows_v2","table_update_row","table_update_rows_by_filter","table_upsert_row","tailscale_authorize_device","tailscale_create_auth_key","tailscale_delete_auth_key","tailscale_delete_device","tailscale_delete_user","tailscale_expire_device_key","tailscale_get_acl","tailscale_get_auth_key","tailscale_get_device","tailscale_get_device_routes","tailscale_get_dns_preferences","tailscale_get_dns_searchpaths","tailscale_list_auth_keys","tailscale_list_devices","tailscale_list_dns_nameservers","tailscale_list_users","tailscale_set_acl","tailscale_set_device_routes","tailscale_set_device_tags","tailscale_set_dns_nameservers","tailscale_set_dns_preferences","tailscale_set_dns_searchpaths","tailscale_suspend_user","tailscale_update_device_key","tavily_crawl","tavily_extract","tavily_map","tavily_search","telegram_copy_message","telegram_delete_message","telegram_edit_message_text","telegram_forward_message","telegram_get_chat","telegram_get_chat_member","telegram_message","telegram_pin_message","telegram_send_animation","telegram_send_audio","telegram_send_chat_action","telegram_send_contact","telegram_send_document","telegram_send_location","telegram_send_photo","telegram_send_poll","telegram_send_video","telegram_set_message_reaction","telegram_unpin_message","temporal_cancel_workflow","temporal_count_workflows","temporal_create_schedule","temporal_delete_schedule","temporal_describe_schedule","temporal_describe_task_queue","temporal_describe_workflow","temporal_get_workflow_history","temporal_list_schedules","temporal_list_workflows","temporal_pause_schedule","temporal_query_workflow","temporal_reset_workflow","temporal_signal_with_start","temporal_signal_workflow","temporal_start_workflow","temporal_terminate_workflow","temporal_trigger_schedule","temporal_unpause_schedule","temporal_update_workflow","textract_analyze_expense","textract_analyze_id","textract_parser","textract_parser_v2","thinking_tool","thrive_add_audience_managers","thrive_add_audience_members","thrive_add_user_tags","thrive_create_assignment","thrive_create_audience","thrive_create_completion","thrive_create_user","thrive_delete_assignment","thrive_delete_audience","thrive_delete_user","thrive_get_activity","thrive_get_assignment","thrive_get_audience","thrive_get_completion","thrive_get_content","thrive_get_cpd_category","thrive_get_cpd_entry","thrive_get_cpd_requirement","thrive_get_enrolment","thrive_get_skill_levels","thrive_get_tag","thrive_get_user_by_id","thrive_get_user_by_ref","thrive_list_assignments","thrive_list_audience_managers","thrive_list_audience_members","thrive_list_audiences","thrive_list_completions","thrive_list_enrolments","thrive_list_tags","thrive_query_activities","thrive_query_content","thrive_query_cpd_categories","thrive_query_cpd_entries","thrive_query_cpd_requirements","thrive_query_cpd_user_summaries","thrive_remove_audience_manager","thrive_remove_audience_member","thrive_remove_user_tags","thrive_replace_audience_managers","thrive_replace_audience_members","thrive_search_users","thrive_suspend_user","thrive_update_assignment","thrive_update_audience","thrive_update_user","thrive_update_user_skills","tiktok_get_post_status","tiktok_get_user","tiktok_list_videos","tiktok_query_videos","tiktok_upload_video_draft","tinybird_append_datasource","tinybird_delete_datasource_rows","tinybird_events","tinybird_get_job","tinybird_query","tinybird_query_pipe","tinybird_truncate_datasource","tinyfish_cancel_run","tinyfish_fetch","tinyfish_get_run","tinyfish_list_runs","tinyfish_list_vault_items","tinyfish_run","tinyfish_run_async","tinyfish_search","trello_add_checklist","trello_add_checklist_item","trello_add_comment","trello_add_label","trello_add_member","trello_create_board","trello_create_card","trello_create_list","trello_delete_card","trello_get_actions","trello_get_board","trello_get_card","trello_list_cards","trello_list_lists","trello_list_members","trello_remove_label","trello_remove_member","trello_search","trello_update_card","trello_update_checklist_item","trello_update_list","trigger_dev_activate_schedule","trigger_dev_add_run_tags","trigger_dev_batch_trigger_task","trigger_dev_cancel_run","trigger_dev_complete_waitpoint_token","trigger_dev_create_env_var","trigger_dev_create_schedule","trigger_dev_create_waitpoint_token","trigger_dev_deactivate_schedule","trigger_dev_delete_env_var","trigger_dev_delete_schedule","trigger_dev_execute_query","trigger_dev_get_batch","trigger_dev_get_batch_results","trigger_dev_get_deployment","trigger_dev_get_env_var","trigger_dev_get_latest_deployment","trigger_dev_get_query_schema","trigger_dev_get_queue","trigger_dev_get_run","trigger_dev_get_run_events","trigger_dev_get_run_result","trigger_dev_get_run_trace","trigger_dev_get_schedule","trigger_dev_get_waitpoint_token","trigger_dev_import_env_vars","trigger_dev_list_deployments","trigger_dev_list_env_vars","trigger_dev_list_queues","trigger_dev_list_runs","trigger_dev_list_schedules","trigger_dev_list_timezones","trigger_dev_list_waitpoint_tokens","trigger_dev_override_queue_concurrency","trigger_dev_pause_queue","trigger_dev_promote_deployment","trigger_dev_replay_run","trigger_dev_reschedule_run","trigger_dev_reset_queue_concurrency","trigger_dev_resume_queue","trigger_dev_trigger_task","trigger_dev_update_env_var","trigger_dev_update_run_metadata","trigger_dev_update_schedule","tts_azure","tts_cartesia","tts_deepgram","tts_elevenlabs","tts_google","tts_openai","tts_playht","twilio_send_sms","twilio_voice_get_recording","twilio_voice_list_calls","twilio_voice_make_call","typeform_create_form","typeform_delete_form","typeform_files","typeform_get_form","typeform_insights","typeform_list_forms","typeform_responses","typeform_update_form","upstash_redis_command","upstash_redis_delete","upstash_redis_exists","upstash_redis_expire","upstash_redis_get","upstash_redis_hget","upstash_redis_hgetall","upstash_redis_hset","upstash_redis_incr","upstash_redis_incrby","upstash_redis_keys","upstash_redis_lpush","upstash_redis_lrange","upstash_redis_set","upstash_redis_setnx","upstash_redis_ttl","uptimerobot_create_alert_contact","uptimerobot_create_maintenance_window","uptimerobot_create_monitor","uptimerobot_create_psp","uptimerobot_delete_alert_contact","uptimerobot_delete_maintenance_window","uptimerobot_delete_monitor","uptimerobot_delete_psp","uptimerobot_get_account","uptimerobot_get_alert_contact","uptimerobot_get_incident","uptimerobot_get_maintenance_window","uptimerobot_get_monitor","uptimerobot_get_psp","uptimerobot_list_alert_contacts","uptimerobot_list_incidents","uptimerobot_list_maintenance_windows","uptimerobot_list_monitors","uptimerobot_list_psps","uptimerobot_pause_monitor","uptimerobot_start_monitor","uptimerobot_update_maintenance_window","uptimerobot_update_monitor","uptimerobot_update_psp","vanta_download_document_file","vanta_get_control","vanta_get_document","vanta_get_framework","vanta_get_person","vanta_get_policy","vanta_get_risk_scenario","vanta_get_test","vanta_get_vendor","vanta_get_vulnerable_asset","vanta_list_control_documents","vanta_list_control_tests","vanta_list_controls","vanta_list_document_uploads","vanta_list_documents","vanta_list_framework_controls","vanta_list_frameworks","vanta_list_monitored_computers","vanta_list_people","vanta_list_policies","vanta_list_risk_scenarios","vanta_list_test_entities","vanta_list_tests","vanta_list_vendors","vanta_list_vulnerabilities","vanta_list_vulnerability_remediations","vanta_list_vulnerable_assets","vanta_submit_document","vanta_upload_document_file","vercel_add_domain","vercel_add_project_domain","vercel_cancel_deployment","vercel_create_alias","vercel_create_check","vercel_create_deployment","vercel_create_dns_record","vercel_create_edge_config","vercel_create_env_var","vercel_create_project","vercel_create_webhook","vercel_delete_alias","vercel_delete_deployment","vercel_delete_dns_record","vercel_delete_domain","vercel_delete_edge_config","vercel_delete_env_var","vercel_delete_project","vercel_delete_webhook","vercel_get_alias","vercel_get_check","vercel_get_deployment","vercel_get_deployment_events","vercel_get_domain","vercel_get_domain_config","vercel_get_edge_config","vercel_get_edge_config_items","vercel_get_env_vars","vercel_get_project","vercel_get_team","vercel_get_user","vercel_get_webhook","vercel_list_aliases","vercel_list_checks","vercel_list_deployment_files","vercel_list_deployments","vercel_list_dns_records","vercel_list_domains","vercel_list_edge_configs","vercel_list_project_domains","vercel_list_projects","vercel_list_team_members","vercel_list_teams","vercel_list_webhooks","vercel_pause_project","vercel_promote_deployment","vercel_remove_project_domain","vercel_rerequest_check","vercel_unpause_project","vercel_update_check","vercel_update_dns_record","vercel_update_edge_config_items","vercel_update_env_var","vercel_update_project","vercel_update_project_domain","vercel_verify_project_domain","video_falai","video_luma","video_minimax","video_runway","video_veo","vision_tool","vision_tool_v2","wealthbox_read_contact","wealthbox_read_note","wealthbox_read_task","wealthbox_write_contact","wealthbox_write_note","wealthbox_write_task","webflow_create_item","webflow_delete_item","webflow_get_item","webflow_list_items","webflow_update_item","webhook_request","whatsapp_get_media","whatsapp_mark_read","whatsapp_send_interactive","whatsapp_send_media","whatsapp_send_message","whatsapp_send_reaction","whatsapp_send_template","whatsapp_upload_media","wikipedia_content","wikipedia_random","wikipedia_search","wikipedia_summary","windchill_check_in_document","windchill_check_in_documents","windchill_check_out_document","windchill_check_out_documents","windchill_create_document","windchill_create_documents","windchill_delete_document","windchill_delete_documents","windchill_download_attachment","windchill_download_primary_content","windchill_get_document","windchill_get_document_structure","windchill_get_primary_content","windchill_get_valid_state_transitions","windchill_list_attachments","windchill_list_documents","windchill_revise_document","windchill_revise_documents","windchill_set_lifecycle_state","windchill_undo_check_out_document","windchill_undo_check_out_documents","windchill_update_common_properties","windchill_update_document","windchill_update_document_security_labels","windchill_update_documents","windchill_upload_attachments","windchill_upload_primary_content","wiza_company_enrichment","wiza_get_credits","wiza_individual_reveal","wiza_prospect_search","wordpress_create_category","wordpress_create_comment","wordpress_create_page","wordpress_create_post","wordpress_create_tag","wordpress_delete_category","wordpress_delete_comment","wordpress_delete_media","wordpress_delete_page","wordpress_delete_post","wordpress_delete_tag","wordpress_get_category","wordpress_get_current_user","wordpress_get_media","wordpress_get_page","wordpress_get_post","wordpress_get_tag","wordpress_get_user","wordpress_list_categories","wordpress_list_comments","wordpress_list_media","wordpress_list_pages","wordpress_list_posts","wordpress_list_tags","wordpress_list_users","wordpress_search_content","wordpress_update_category","wordpress_update_comment","wordpress_update_page","wordpress_update_post","wordpress_update_tag","wordpress_upload_media","workday_assign_onboarding","workday_change_job","workday_create_prehire","workday_get_compensation","workday_get_organizations","workday_get_worker","workday_hire_employee","workday_list_workers","workday_terminate_worker","workday_update_worker","workflow_executor","x_create_bookmark","x_create_tweet","x_delete_bookmark","x_delete_tweet","x_get_blocking","x_get_bookmarks","x_get_followers","x_get_following","x_get_liked_tweets","x_get_liking_users","x_get_me","x_get_personalized_trends","x_get_quote_tweets","x_get_retweeted_by","x_get_trends_by_woeid","x_get_tweets_by_ids","x_get_usage","x_get_user_mentions","x_get_user_timeline","x_get_user_tweets","x_hide_reply","x_manage_block","x_manage_follow","x_manage_like","x_manage_mute","x_manage_retweet","x_read","x_search","x_search_tweets","x_search_users","x_user","x_write","youtube_channel_info","youtube_channel_playlists","youtube_channel_videos","youtube_comments","youtube_playlist_items","youtube_search","youtube_trending","youtube_video_categories","youtube_video_details","zendesk_autocomplete_organizations","zendesk_create_organization","zendesk_create_organizations_bulk","zendesk_create_ticket","zendesk_create_tickets_bulk","zendesk_create_user","zendesk_create_users_bulk","zendesk_delete_organization","zendesk_delete_ticket","zendesk_delete_user","zendesk_get_current_user","zendesk_get_organization","zendesk_get_organizations","zendesk_get_ticket","zendesk_get_tickets","zendesk_get_user","zendesk_get_users","zendesk_merge_tickets","zendesk_search","zendesk_search_count","zendesk_search_users","zendesk_update_organization","zendesk_update_ticket","zendesk_update_tickets_bulk","zendesk_update_user","zendesk_update_users_bulk","zep_add_messages","zep_add_user","zep_create_thread","zep_delete_thread","zep_get_context","zep_get_messages","zep_get_threads","zep_get_user","zep_get_user_threads","zerobounce_get_credits","zerobounce_verify_email","zoho_desk_add_comment","zoho_desk_get_attachment","zoho_desk_get_contact","zoho_desk_get_thread","zoho_desk_get_ticket","zoho_desk_list_comments","zoho_desk_list_organizations","zoho_desk_list_threads","zoho_desk_list_tickets","zoho_desk_update_ticket","zoom_create_meeting","zoom_delete_meeting","zoom_delete_recording","zoom_get_meeting","zoom_get_meeting_invitation","zoom_get_meeting_recordings","zoom_list_meetings","zoom_list_past_participants","zoom_list_recordings","zoom_update_meeting","zoominfo_enrich_companies","zoominfo_enrich_contacts","zoominfo_search_companies","zoominfo_search_contacts","zoominfo_search_intent","zoominfo_search_news"]' + '["a2a_cancel_task","a2a_get_agent_card","a2a_get_task","a2a_send_message","affinity_batch_update_entity_fields","affinity_batch_update_list_entry_fields","affinity_create_list","affinity_create_list_field_dropdown_option","affinity_create_merge","affinity_create_note","affinity_create_reminder","affinity_delete_list_field_dropdown_option","affinity_delete_note","affinity_get_company","affinity_get_current_user","affinity_get_entity_field_value","affinity_get_list","affinity_get_list_entry","affinity_get_list_entry_field","affinity_get_list_field_dropdown_option","affinity_get_merge","affinity_get_merge_task","affinity_get_note","affinity_get_opportunity","affinity_get_person","affinity_get_saved_view","affinity_get_transcript","affinity_get_user","affinity_list_calls","affinity_list_chat_messages","affinity_list_companies","affinity_list_coworker_connections","affinity_list_emails","affinity_list_entity_field_values","affinity_list_entity_list_entries","affinity_list_entity_lists","affinity_list_entity_notes","affinity_list_entity_relationships","affinity_list_field_dropdown_options","affinity_list_field_metadata","affinity_list_field_value_changes","affinity_list_investor_executive_connections","affinity_list_list_entries","affinity_list_list_entry_field_value_changes","affinity_list_list_entry_fields","affinity_list_list_field_dropdown_options","affinity_list_list_fields","affinity_list_lists","affinity_list_meetings","affinity_list_merge_tasks","affinity_list_merges","affinity_list_note_attached_companies","affinity_list_note_attached_opportunities","affinity_list_note_attached_persons","affinity_list_note_replies","affinity_list_notes","affinity_list_opportunities","affinity_list_persons","affinity_list_reminders","affinity_list_saved_view_entries","affinity_list_saved_views","affinity_list_transcript_fragments","affinity_list_transcripts","affinity_list_users","affinity_search_companies","affinity_search_files","affinity_search_list_entries","affinity_search_notes","affinity_search_persons","affinity_semantic_search","affinity_update_entity_field_value","affinity_update_list_entry_field","affinity_update_list_field_dropdown_option","affinity_update_note","agentmail_create_draft","agentmail_create_inbox","agentmail_delete_draft","agentmail_delete_inbox","agentmail_delete_thread","agentmail_forward_message","agentmail_get_draft","agentmail_get_inbox","agentmail_get_message","agentmail_get_thread","agentmail_list_drafts","agentmail_list_inboxes","agentmail_list_messages","agentmail_list_threads","agentmail_reply_message","agentmail_send_draft","agentmail_send_message","agentmail_update_draft","agentmail_update_inbox","agentmail_update_message","agentmail_update_thread","agentphone_create_call","agentphone_create_contact","agentphone_create_number","agentphone_delete_contact","agentphone_get_call","agentphone_get_call_transcript","agentphone_get_contact","agentphone_get_conversation","agentphone_get_conversation_messages","agentphone_get_number_messages","agentphone_get_usage","agentphone_get_usage_daily","agentphone_get_usage_monthly","agentphone_list_calls","agentphone_list_contacts","agentphone_list_conversations","agentphone_list_numbers","agentphone_react_to_message","agentphone_release_number","agentphone_send_message","agentphone_update_contact","agentphone_update_conversation","agiloft_async_status","agiloft_attach_file","agiloft_attachment_info","agiloft_create_record","agiloft_delete_record","agiloft_get_choice_line_id","agiloft_list_tables","agiloft_lock_record","agiloft_nlp_search","agiloft_read_record","agiloft_remove_attachment","agiloft_retrieve_attachment","agiloft_run_action_button","agiloft_saved_search","agiloft_search_records","agiloft_select_records","agiloft_update_record","agiloft_upsert_record","ahrefs_anchors","ahrefs_backlinks","ahrefs_backlinks_stats","ahrefs_batch_analysis","ahrefs_broken_backlinks","ahrefs_domain_rating","ahrefs_domain_rating_history","ahrefs_keyword_overview","ahrefs_keywords_history","ahrefs_metrics","ahrefs_metrics_history","ahrefs_organic_competitors","ahrefs_organic_keywords","ahrefs_paid_pages","ahrefs_rank_tracker_competitors_overview","ahrefs_rank_tracker_competitors_stats","ahrefs_rank_tracker_overview","ahrefs_rank_tracker_serp_overview","ahrefs_refdomains_history","ahrefs_referring_domains","ahrefs_related_terms","ahrefs_site_audit_page_explorer","ahrefs_top_pages","airtable_create_records","airtable_delete_records","airtable_get_base_schema","airtable_get_record","airtable_list_bases","airtable_list_records","airtable_list_tables","airtable_update_multiple_records","airtable_update_record","airtable_upsert_records","airweave_search","algolia_add_record","algolia_batch_operations","algolia_browse_records","algolia_clear_records","algolia_copy_move_index","algolia_delete_by_filter","algolia_delete_index","algolia_delete_record","algolia_get_record","algolia_get_records","algolia_get_settings","algolia_get_task_status","algolia_list_indices","algolia_partial_update_record","algolia_search","algolia_update_settings","amplitude_event_segmentation","amplitude_funnels","amplitude_get_active_users","amplitude_get_revenue","amplitude_group_identify","amplitude_identify_user","amplitude_list_events","amplitude_realtime_active_users","amplitude_retention","amplitude_send_event","amplitude_user_activity","amplitude_user_profile","amplitude_user_search","apify_get_dataset_items","apify_get_run","apify_run_actor_async","apify_run_actor_sync","apify_run_task","apollo_account_bulk_create","apollo_account_bulk_update","apollo_account_create","apollo_account_search","apollo_account_update","apollo_contact_bulk_create","apollo_contact_bulk_update","apollo_contact_create","apollo_contact_search","apollo_contact_update","apollo_email_accounts","apollo_opportunity_create","apollo_opportunity_get","apollo_opportunity_search","apollo_opportunity_update","apollo_organization_bulk_enrich","apollo_organization_enrich","apollo_organization_search","apollo_people_bulk_enrich","apollo_people_enrich","apollo_people_search","apollo_sequence_add_contacts","apollo_sequence_search","apollo_task_create","apollo_task_search","appconfig_create_application","appconfig_create_configuration_profile","appconfig_create_environment","appconfig_create_hosted_configuration_version","appconfig_delete_application","appconfig_delete_configuration_profile","appconfig_delete_environment","appconfig_delete_hosted_configuration_version","appconfig_get_application","appconfig_get_configuration","appconfig_get_configuration_profile","appconfig_get_deployment","appconfig_get_environment","appconfig_get_hosted_configuration_version","appconfig_list_applications","appconfig_list_configuration_profiles","appconfig_list_deployment_strategies","appconfig_list_deployments","appconfig_list_environments","appconfig_list_hosted_configuration_versions","appconfig_start_deployment","appconfig_stop_deployment","appconfig_update_application","appconfig_update_configuration_profile","appconfig_update_environment","arxiv_get_author_papers","arxiv_get_paper","arxiv_search","asana_add_comment","asana_add_followers","asana_create_project","asana_create_section","asana_create_subtask","asana_create_task","asana_delete_task","asana_get_project","asana_get_projects","asana_get_task","asana_list_sections","asana_list_workspaces","asana_search_tasks","asana_update_task","ashby_add_candidate_tag","ashby_anonymize_candidate","ashby_change_application_source","ashby_change_application_stage","ashby_create_application","ashby_create_candidate","ashby_create_note","ashby_delete_application","ashby_get_application","ashby_get_candidate","ashby_get_job","ashby_get_job_posting","ashby_get_offer","ashby_get_opening","ashby_list_application_feedback","ashby_list_application_history","ashby_list_applications","ashby_list_archive_reasons","ashby_list_candidate_tags","ashby_list_candidates","ashby_list_custom_fields","ashby_list_departments","ashby_list_interview_plans","ashby_list_interview_stages","ashby_list_interviews","ashby_list_job_postings","ashby_list_jobs","ashby_list_locations","ashby_list_notes","ashby_list_offers","ashby_list_openings","ashby_list_sources","ashby_list_users","ashby_remove_candidate_tag","ashby_search_candidates","ashby_search_jobs","ashby_search_openings","ashby_search_users","ashby_set_custom_field_value","ashby_set_custom_field_values","ashby_transfer_application","ashby_update_candidate","ashby_upload_candidate_file","ashby_upload_resume","athena_batch_get_query_execution","athena_create_named_query","athena_delete_named_query","athena_get_named_query","athena_get_query_execution","athena_get_query_results","athena_list_databases","athena_list_named_queries","athena_list_query_executions","athena_list_table_metadata","athena_start_query","athena_stop_query","attio_assert_record","attio_create_attribute","attio_create_comment","attio_create_list","attio_create_list_entry","attio_create_note","attio_create_object","attio_create_record","attio_create_task","attio_create_webhook","attio_delete_comment","attio_delete_list_entry","attio_delete_note","attio_delete_record","attio_delete_task","attio_delete_webhook","attio_get_attribute","attio_get_comment","attio_get_list","attio_get_list_entry","attio_get_member","attio_get_note","attio_get_object","attio_get_record","attio_get_task","attio_get_thread","attio_get_webhook","attio_list_attributes","attio_list_lists","attio_list_members","attio_list_notes","attio_list_objects","attio_list_records","attio_list_tasks","attio_list_threads","attio_list_webhooks","attio_query_list_entries","attio_search_records","attio_update_attribute","attio_update_list","attio_update_list_entry","attio_update_object","attio_update_record","attio_update_task","attio_update_webhook","azure_data_explorer_create_table","azure_data_explorer_drop_table","azure_data_explorer_ingest_from_query","azure_data_explorer_ingest_inline","azure_data_explorer_list_databases","azure_data_explorer_list_functions","azure_data_explorer_list_tables","azure_data_explorer_management","azure_data_explorer_query","azure_data_explorer_show_database_schema","azure_data_explorer_show_ingestion_failures","azure_data_explorer_show_operations","azure_data_explorer_show_table_details","azure_data_explorer_show_table_schema","azure_devops_add_comment","azure_devops_create_work_item","azure_devops_get_build_log","azure_devops_get_build_timeline","azure_devops_get_comments","azure_devops_get_pipeline","azure_devops_get_pipeline_run","azure_devops_get_work_item","azure_devops_get_work_items_batch","azure_devops_get_work_items_between_builds","azure_devops_list_build_logs","azure_devops_list_builds","azure_devops_list_pipeline_runs","azure_devops_list_pipelines","azure_devops_query_work_items","azure_devops_update_work_item","bitbucket_approve_pull_request","bitbucket_create_branch","bitbucket_create_pull_request","bitbucket_create_pull_request_comment","bitbucket_decline_pull_request","bitbucket_delete_branch","bitbucket_get_commit","bitbucket_get_file","bitbucket_get_file_metadata","bitbucket_get_pipeline","bitbucket_get_pipeline_step_log","bitbucket_get_pull_request","bitbucket_get_pull_request_diff","bitbucket_get_pull_request_diffstat","bitbucket_get_pull_request_merge_task_status","bitbucket_get_repository","bitbucket_list_branches","bitbucket_list_commits","bitbucket_list_directory","bitbucket_list_pipeline_steps","bitbucket_list_pipelines","bitbucket_list_pull_request_comments","bitbucket_list_pull_request_commit_statuses","bitbucket_list_pull_requests","bitbucket_list_repositories","bitbucket_list_workspaces","bitbucket_merge_pull_request","bitbucket_request_pull_request_changes","bitbucket_stop_pipeline","bitbucket_trigger_pipeline","box_copy_file","box_create_folder","box_delete_file","box_delete_folder","box_download_file","box_get_file_info","box_list_folder_items","box_search","box_sign_cancel_request","box_sign_create_request","box_sign_get_request","box_sign_list_requests","box_sign_resend_request","box_update_file","box_upload_file","brandfetch_get_brand","brandfetch_search","brex_archive_budget","brex_create_budget","brex_create_spend_limit","brex_create_transfer","brex_create_vendor","brex_get_budget","brex_get_cash_account","brex_get_company","brex_get_current_user","brex_get_expense","brex_get_spend_limit","brex_get_transfer","brex_get_user","brex_get_vendor","brex_list_budgets","brex_list_card_accounts","brex_list_card_statements","brex_list_card_transactions","brex_list_cards","brex_list_cash_accounts","brex_list_cash_statements","brex_list_cash_transactions","brex_list_departments","brex_list_expenses","brex_list_locations","brex_list_spend_limits","brex_list_titles","brex_list_transfers","brex_list_users","brex_list_vendors","brex_match_receipt","brex_update_expense","brex_update_vendor","brex_upload_receipt","brightdata_cancel_snapshot","brightdata_discover","brightdata_download_snapshot","brightdata_scrape_dataset","brightdata_scrape_url","brightdata_serp_search","brightdata_snapshot_status","brightdata_sync_scrape","browser_use_run_task","buffer_create_idea","buffer_create_post","buffer_delete_post","buffer_edit_post","buffer_get_account","buffer_get_channels","buffer_get_idea_groups","buffer_get_ideas","buffer_get_post","buffer_get_posts","calcom_cancel_booking","calcom_confirm_booking","calcom_create_booking","calcom_create_event_type","calcom_create_schedule","calcom_decline_booking","calcom_delete_event_type","calcom_delete_schedule","calcom_get_booking","calcom_get_default_schedule","calcom_get_event_type","calcom_get_schedule","calcom_get_slots","calcom_list_bookings","calcom_list_event_types","calcom_list_schedules","calcom_reschedule_booking","calcom_update_event_type","calcom_update_schedule","calendly_cancel_event","calendly_create_event_invitee","calendly_create_invitee_no_show","calendly_create_scheduling_link","calendly_create_webhook","calendly_delete_invitee_no_show","calendly_delete_webhook","calendly_get_current_user","calendly_get_event_invitee","calendly_get_event_type","calendly_get_scheduled_event","calendly_get_user","calendly_list_event_invitees","calendly_list_event_type_available_times","calendly_list_event_types","calendly_list_organization_memberships","calendly_list_routing_form_submissions","calendly_list_routing_forms","calendly_list_scheduled_events","calendly_list_user_availability_schedules","calendly_list_user_busy_times","calendly_list_webhooks","cbinsights_chat","cbinsights_get_commercial_maturity_history","cbinsights_get_exit_probability_history","cbinsights_get_mosaic_history","cbinsights_get_org_business_relationships","cbinsights_get_org_funding_window","cbinsights_get_org_fundings","cbinsights_get_org_investments","cbinsights_get_org_management_and_board","cbinsights_get_org_outlook","cbinsights_get_org_portfolio_exits","cbinsights_get_org_revenue","cbinsights_get_scouting_report","cbinsights_get_strategy_map","cbinsights_list_business_relationships","cbinsights_list_funding_window","cbinsights_list_fundings","cbinsights_list_investments","cbinsights_list_management_and_board","cbinsights_list_outlook","cbinsights_list_portfolio_exits","cbinsights_list_revenue","cbinsights_lookup_organizations","cbinsights_rag","cbinsights_search_firmographics","circleback_add_tag_to_meetings","circleback_create_tag","circleback_delete_action_item","circleback_delete_meeting","circleback_delete_tag","circleback_get_company","circleback_get_meeting","circleback_get_person","circleback_get_transcript","circleback_list_action_items","circleback_list_calendar_events","circleback_list_companies","circleback_list_meetings","circleback_list_people","circleback_list_tags","circleback_remove_tag_from_meetings","circleback_search_meetings","circleback_update_action_item","circleback_update_meeting","circleback_update_tag","clay_populate","clerk_add_organization_member","clerk_ban_user","clerk_create_actor_token","clerk_create_allowlist_identifier","clerk_create_blocklist_identifier","clerk_create_organization","clerk_create_organization_invitation","clerk_create_user","clerk_delete_allowlist_identifier","clerk_delete_blocklist_identifier","clerk_delete_organization","clerk_delete_user","clerk_get_jwt_template","clerk_get_organization","clerk_get_session","clerk_get_user","clerk_get_user_oauth_token","clerk_list_allowlist_identifiers","clerk_list_blocklist_identifiers","clerk_list_jwt_templates","clerk_list_organization_invitations","clerk_list_organization_memberships","clerk_list_organizations","clerk_list_sessions","clerk_list_users","clerk_lock_user","clerk_remove_organization_member","clerk_revoke_actor_token","clerk_revoke_session","clerk_unban_user","clerk_unlock_user","clerk_update_organization","clerk_update_organization_membership","clerk_update_user","clickhouse_count_rows","clickhouse_create_database","clickhouse_create_table","clickhouse_delete","clickhouse_describe_table","clickhouse_drop_database","clickhouse_drop_partition","clickhouse_drop_table","clickhouse_execute","clickhouse_insert","clickhouse_insert_rows","clickhouse_introspect","clickhouse_kill_query","clickhouse_list_clusters","clickhouse_list_databases","clickhouse_list_mutations","clickhouse_list_partitions","clickhouse_list_running_queries","clickhouse_list_tables","clickhouse_optimize_table","clickhouse_query","clickhouse_rename_table","clickhouse_show_create_table","clickhouse_table_stats","clickhouse_truncate_table","clickhouse_update","clickup_add_tag_to_task","clickup_create_checklist","clickup_create_checklist_item","clickup_create_comment","clickup_create_folder","clickup_create_list","clickup_create_task","clickup_create_time_entry","clickup_delete_checklist","clickup_delete_checklist_item","clickup_delete_comment","clickup_delete_task","clickup_delete_time_entry","clickup_get_comments","clickup_get_custom_fields","clickup_get_folders","clickup_get_list_members","clickup_get_lists","clickup_get_running_timer","clickup_get_space_tags","clickup_get_spaces","clickup_get_task","clickup_get_task_members","clickup_get_tasks","clickup_get_time_entries","clickup_get_workspaces","clickup_remove_custom_field_value","clickup_remove_tag_from_task","clickup_search_tasks","clickup_set_custom_field_value","clickup_start_timer","clickup_stop_timer","clickup_update_checklist","clickup_update_checklist_item","clickup_update_comment","clickup_update_task","clickup_update_time_entry","clickup_upload_attachment","cloudflare_create_access_application","cloudflare_create_access_policy","cloudflare_create_access_service_token","cloudflare_create_dns_record","cloudflare_create_r2_bucket","cloudflare_create_rate_limit_rule","cloudflare_create_ruleset","cloudflare_create_ruleset_rule","cloudflare_create_zone","cloudflare_delete_access_application","cloudflare_delete_access_policy","cloudflare_delete_dns_record","cloudflare_delete_r2_bucket","cloudflare_delete_ruleset_rule","cloudflare_delete_zone","cloudflare_dns_analytics","cloudflare_get_access_application","cloudflare_get_r2_bucket","cloudflare_get_ruleset","cloudflare_get_ruleset_entrypoint","cloudflare_get_tunnel","cloudflare_get_tunnel_configuration","cloudflare_get_worker_script_settings","cloudflare_get_zone","cloudflare_get_zone_settings","cloudflare_list_access_applications","cloudflare_list_access_groups","cloudflare_list_access_identity_providers","cloudflare_list_access_policies","cloudflare_list_access_service_tokens","cloudflare_list_certificates","cloudflare_list_dns_records","cloudflare_list_managed_ruleset_overrides","cloudflare_list_r2_buckets","cloudflare_list_rate_limit_rules","cloudflare_list_rulesets","cloudflare_list_tunnels","cloudflare_list_worker_routes","cloudflare_list_worker_scripts","cloudflare_list_zones","cloudflare_purge_cache","cloudflare_revoke_access_service_token","cloudflare_update_access_application","cloudflare_update_access_policy","cloudflare_update_dns_record","cloudflare_update_rate_limit_rule","cloudflare_update_ruleset_rule","cloudflare_update_zone_setting","cloudformation_cancel_update_stack","cloudformation_create_change_set","cloudformation_create_stack","cloudformation_delete_stack","cloudformation_describe_change_set","cloudformation_describe_stack_drift_detection_status","cloudformation_describe_stack_events","cloudformation_describe_stacks","cloudformation_detect_stack_drift","cloudformation_execute_change_set","cloudformation_get_template","cloudformation_get_template_summary","cloudformation_list_stack_resources","cloudformation_update_stack","cloudformation_validate_template","cloudwatch_describe_alarm_history","cloudwatch_describe_alarms","cloudwatch_describe_log_groups","cloudwatch_describe_log_streams","cloudwatch_filter_log_events","cloudwatch_get_log_events","cloudwatch_get_metric_statistics","cloudwatch_list_metrics","cloudwatch_mute_alarm","cloudwatch_put_log_group_retention","cloudwatch_put_metric_data","cloudwatch_query_logs","cloudwatch_unmute_alarm","codepipeline_disable_stage_transition","codepipeline_enable_stage_transition","codepipeline_get_pipeline","codepipeline_get_pipeline_execution","codepipeline_get_pipeline_state","codepipeline_list_action_executions","codepipeline_list_pipeline_executions","codepipeline_list_pipelines","codepipeline_put_approval_result","codepipeline_retry_stage_execution","codepipeline_start_execution","codepipeline_stop_execution","confluence_add_label","confluence_create_blogpost","confluence_create_comment","confluence_create_page","confluence_create_page_property","confluence_create_space","confluence_create_space_property","confluence_delete_attachment","confluence_delete_blogpost","confluence_delete_comment","confluence_delete_label","confluence_delete_page","confluence_delete_page_property","confluence_delete_space","confluence_delete_space_property","confluence_get_blogpost","confluence_get_page_ancestors","confluence_get_page_children","confluence_get_page_descendants","confluence_get_page_version","confluence_get_pages_by_label","confluence_get_space","confluence_get_task","confluence_get_user","confluence_list_attachments","confluence_list_blogposts","confluence_list_blogposts_in_space","confluence_list_comments","confluence_list_labels","confluence_list_page_properties","confluence_list_page_versions","confluence_list_pages_in_space","confluence_list_space_labels","confluence_list_space_permissions","confluence_list_space_properties","confluence_list_spaces","confluence_list_tasks","confluence_retrieve","confluence_search","confluence_search_in_space","confluence_update","confluence_update_blogpost","confluence_update_comment","confluence_update_space","confluence_update_task","confluence_upload_attachment","context_dev_classify_naics","context_dev_classify_sic","context_dev_crawl","context_dev_extract","context_dev_extract_product","context_dev_extract_products","context_dev_get_brand","context_dev_get_brand_by_email","context_dev_get_brand_by_name","context_dev_get_brand_by_ticker","context_dev_identify_transaction","context_dev_map","context_dev_scrape_fonts","context_dev_scrape_html","context_dev_scrape_images","context_dev_scrape_markdown","context_dev_scrape_styleguide","context_dev_screenshot","context_dev_search","convex_action","convex_document_deltas","convex_list_documents","convex_list_tables","convex_mutation","convex_query","convex_run_function","crowdstrike_create_indicators","crowdstrike_delete_indicators","crowdstrike_delete_rtr_session","crowdstrike_execute_rtr_command","crowdstrike_get_alert_details","crowdstrike_get_case_details","crowdstrike_get_host_group_details","crowdstrike_get_indicator_details","crowdstrike_get_rtr_command_status","crowdstrike_get_sensor_aggregates","crowdstrike_get_sensor_details","crowdstrike_get_vulnerability_details","crowdstrike_init_rtr_session","crowdstrike_perform_host_action","crowdstrike_perform_host_group_action","crowdstrike_query_alerts","crowdstrike_query_cases","crowdstrike_query_host_groups","crowdstrike_query_indicators","crowdstrike_query_sensors","crowdstrike_query_vulnerabilities","crowdstrike_update_alerts","crowdstrike_update_indicators","crunchbase_autocomplete","crunchbase_get_acquisition","crunchbase_get_entity","crunchbase_get_entity_card","crunchbase_get_fields_metadata","crunchbase_get_funding_round","crunchbase_get_organization","crunchbase_get_person","crunchbase_list_deleted_entities","crunchbase_search_acquisitions","crunchbase_search_entities","crunchbase_search_funding_rounds","crunchbase_search_organizations","crunchbase_search_people","cursor_add_followup","cursor_add_followup_v2","cursor_delete_agent","cursor_delete_agent_v2","cursor_download_artifact","cursor_download_artifact_v2","cursor_get_agent","cursor_get_agent_v2","cursor_get_api_key_info","cursor_get_api_key_info_v2","cursor_get_conversation","cursor_get_conversation_v2","cursor_launch_agent","cursor_launch_agent_v2","cursor_list_agents","cursor_list_agents_v2","cursor_list_artifacts","cursor_list_artifacts_v2","cursor_list_models","cursor_list_models_v2","cursor_list_repositories","cursor_list_repositories_v2","cursor_stop_agent","cursor_stop_agent_v2","dagster_delete_run","dagster_get_asset","dagster_get_run","dagster_get_run_logs","dagster_launch_run","dagster_list_assets","dagster_list_jobs","dagster_list_runs","dagster_list_schedules","dagster_list_sensors","dagster_materialize_assets","dagster_reexecute_run","dagster_report_asset_materialization","dagster_start_schedule","dagster_start_sensor","dagster_stop_schedule","dagster_stop_sensor","dagster_terminate_run","dagster_wipe_asset","databricks_cancel_run","databricks_execute_sql","databricks_get_cluster","databricks_get_job","databricks_get_run","databricks_get_run_output","databricks_get_statement","databricks_list_clusters","databricks_list_jobs","databricks_list_runs","databricks_list_warehouses","databricks_run_job","datadog_add_incident_todo","datadog_cancel_downtime","datadog_create_dashboard","datadog_create_downtime","datadog_create_event","datadog_create_incident","datadog_create_monitor","datadog_create_slo","datadog_delete_dashboard","datadog_delete_slo","datadog_get_browser_synthetics_results","datadog_get_dashboard","datadog_get_incident","datadog_get_monitor","datadog_get_security_signal","datadog_get_slo","datadog_get_slo_history","datadog_get_synthetics_results","datadog_get_synthetics_test","datadog_list_dashboards","datadog_list_downtimes","datadog_list_incidents","datadog_list_monitors","datadog_list_security_rules","datadog_list_security_signals","datadog_list_services","datadog_list_slos","datadog_list_synthetics_tests","datadog_mute_monitor","datadog_query_logs","datadog_query_timeseries","datadog_search_spans","datadog_send_logs","datadog_submit_metrics","datadog_trigger_synthetics_tests","datadog_unmute_monitor","datadog_update_incident","datadog_update_security_signal_assignee","datadog_update_security_signal_state","datadog_update_slo","datadog_update_synthetics_status","datagma_enrich_company","datagma_enrich_person","datagma_find_email","datagma_find_phone","datagma_get_credits","daytona_create_sandbox","daytona_delete_sandbox","daytona_download_file","daytona_execute_command","daytona_get_sandbox","daytona_git_clone","daytona_list_files","daytona_list_sandboxes","daytona_run_code","daytona_start_sandbox","daytona_stop_sandbox","daytona_upload_file","deployed_block_executor","deployments_deploy","deployments_get_version","deployments_list_versions","deployments_promote","deployments_undeploy","devin_append_session_tags","devin_archive_session","devin_create_session","devin_get_session","devin_get_session_tags","devin_list_session_attachments","devin_list_session_messages","devin_list_sessions","devin_replace_session_tags","devin_send_message","devin_terminate_session","discord_add_reaction","discord_archive_thread","discord_assign_role","discord_ban_member","discord_bulk_delete_messages","discord_create_channel","discord_create_invite","discord_create_role","discord_create_thread","discord_create_webhook","discord_delete_channel","discord_delete_invite","discord_delete_message","discord_delete_role","discord_delete_webhook","discord_edit_message","discord_execute_webhook","discord_get_channel","discord_get_invite","discord_get_member","discord_get_messages","discord_get_pinned_messages","discord_get_server","discord_get_user","discord_get_webhook","discord_join_thread","discord_kick_member","discord_leave_thread","discord_list_channels","discord_list_roles","discord_pin_message","discord_remove_reaction","discord_remove_role","discord_send_message","discord_unban_member","discord_unpin_message","discord_update_channel","discord_update_member","discord_update_role","docusign_create_from_template","docusign_download_document","docusign_get_envelope","docusign_list_envelopes","docusign_list_recipients","docusign_list_templates","docusign_send_envelope","docusign_void_envelope","downdetector_get_company","downdetector_get_company_attribution","downdetector_get_company_baseline","downdetector_get_company_events","downdetector_get_company_incidents","downdetector_get_company_indicators","downdetector_get_company_last_15","downdetector_get_company_status","downdetector_get_provider","downdetector_get_reports","downdetector_get_site_companies","downdetector_list_categories","downdetector_list_incidents","downdetector_list_sites","downdetector_search_companies","dropbox_copy","dropbox_create_folder","dropbox_create_shared_link","dropbox_delete","dropbox_download","dropbox_get_metadata","dropbox_list_folder","dropbox_list_revisions","dropbox_list_shared_links","dropbox_move","dropbox_restore","dropbox_search","dropbox_upload","dropcontact_enrich_contact","dspy_chain_of_thought","dspy_predict","dspy_react","dub_bulk_create_links","dub_bulk_delete_links","dub_bulk_update_links","dub_create_link","dub_create_tag","dub_delete_link","dub_get_analytics","dub_get_events","dub_get_link","dub_get_links_count","dub_get_qr_code","dub_list_domains","dub_list_folders","dub_list_links","dub_list_tags","dub_update_link","dub_upsert_link","duckduckgo_search","dynamodb_delete","dynamodb_get","dynamodb_introspect","dynamodb_put","dynamodb_query","dynamodb_scan","dynamodb_update","dynatrace_add_problem_comment","dynatrace_add_tags","dynatrace_close_problem","dynatrace_create_settings_object","dynatrace_create_slo","dynatrace_delete_problem_comment","dynatrace_delete_settings_object","dynatrace_delete_slo","dynatrace_delete_tag","dynatrace_execute_synthetic_monitors","dynatrace_get_attack","dynatrace_get_audit_logs","dynatrace_get_entity","dynatrace_get_event","dynatrace_get_metric","dynatrace_get_problem","dynatrace_get_problem_comment","dynatrace_get_security_problem","dynatrace_get_settings_object","dynatrace_get_slo","dynatrace_get_synthetic_batch","dynatrace_ingest_event","dynatrace_ingest_logs","dynatrace_ingest_metrics","dynatrace_list_attacks","dynatrace_list_entities","dynatrace_list_entity_types","dynatrace_list_events","dynatrace_list_metrics","dynatrace_list_problem_comments","dynatrace_list_problems","dynatrace_list_remediation_items","dynatrace_list_security_problems","dynatrace_list_settings_objects","dynatrace_list_settings_schemas","dynatrace_list_slos","dynatrace_list_synthetic_monitors","dynatrace_list_tags","dynatrace_mute_security_problem","dynatrace_mute_security_problems","dynatrace_query_metrics","dynatrace_search_logs","dynatrace_unmute_security_problem","dynatrace_unmute_security_problems","dynatrace_update_problem_comment","dynatrace_update_settings_object","dynatrace_update_slo","elasticsearch_bulk","elasticsearch_cluster_health","elasticsearch_cluster_stats","elasticsearch_count","elasticsearch_create_index","elasticsearch_delete_document","elasticsearch_delete_index","elasticsearch_get_document","elasticsearch_get_index","elasticsearch_index_document","elasticsearch_list_indices","elasticsearch_search","elasticsearch_update_document","elevenlabs_audio_isolation","elevenlabs_edit_voice_settings","elevenlabs_get_user","elevenlabs_get_voice","elevenlabs_get_voice_settings","elevenlabs_list_models","elevenlabs_list_voices","elevenlabs_sound_effects","elevenlabs_speech_to_speech","elevenlabs_tts","emailbison_attach_leads_to_campaign","emailbison_attach_tags_to_leads","emailbison_create_campaign","emailbison_create_lead","emailbison_create_tag","emailbison_get_lead","emailbison_list_campaigns","emailbison_list_leads","emailbison_list_replies","emailbison_list_tags","emailbison_update_campaign","emailbison_update_campaign_status","emailbison_update_lead","embeddings_cohere","embeddings_gemini","embeddings_mistral","embeddings_openai","embeddings_openrouter","enrich_check_credits","enrich_company_funding","enrich_company_lookup","enrich_company_revenue","enrich_disposable_email_check","enrich_email_to_ip","enrich_email_to_person_lite","enrich_email_to_phone","enrich_email_to_profile","enrich_find_email","enrich_get_post_details","enrich_ip_to_company","enrich_linkedin_profile","enrich_linkedin_to_personal_email","enrich_linkedin_to_work_email","enrich_phone_finder","enrich_reverse_hash_lookup","enrich_sales_pointer_people","enrich_search_company","enrich_search_company_activities","enrich_search_company_employees","enrich_search_jobs","enrich_search_logo","enrich_search_people","enrich_search_people_activities","enrich_search_post_comments","enrich_search_post_comments_by_url","enrich_search_post_reactions","enrich_search_post_reactions_by_url","enrich_search_posts","enrich_search_similar_companies","enrich_verify_email","enrichment_run","enrow_find_email","enrow_verify_email","exa_agent","exa_answer","exa_find_similar_links","exa_get_contents","exa_search","extend_parser","extend_parser_v2","fathom_get_summary","fathom_get_transcript","fathom_list_meeting_types","fathom_list_meetings","fathom_list_team_members","fathom_list_teams","file_append","file_compress","file_create_folder","file_decompress","file_delete_folder","file_edit","file_fetch","file_get","file_get_content","file_list","file_manage_sharing","file_move","file_parser","file_parser_v2","file_parser_v3","file_read","file_restore_folder","file_search","file_update_folder","file_write","findymail_find_email_from_linkedin","findymail_find_email_from_name","findymail_find_emails_by_domain","findymail_find_employees","findymail_find_phone","findymail_get_company","findymail_get_credits","findymail_lookup_technologies","findymail_reverse_email_lookup","findymail_search_technologies","findymail_verify_email","firecrawl_agent","firecrawl_batch_scrape","firecrawl_batch_scrape_status","firecrawl_cancel_crawl","firecrawl_crawl","firecrawl_crawl_status","firecrawl_credit_usage","firecrawl_extract","firecrawl_extract_status","firecrawl_map","firecrawl_parse","firecrawl_scrape","firecrawl_search","fireflies_add_to_live_meeting","fireflies_create_bite","fireflies_delete_transcript","fireflies_get_transcript","fireflies_get_user","fireflies_list_bites","fireflies_list_contacts","fireflies_list_transcripts","fireflies_list_users","fireflies_upload_audio","flint_create_task","flint_generate_pages","flint_get_task","function_execute","gamma_check_status","gamma_generate","gamma_generate_from_template","gamma_list_folders","gamma_list_themes","github_add_assignees","github_add_assignees_v2","github_add_labels","github_add_labels_v2","github_cancel_workflow_run","github_cancel_workflow_run_v2","github_check_star","github_check_star_v2","github_close_issue","github_close_issue_v2","github_close_pr","github_close_pr_v2","github_comment","github_comment_v2","github_compare_commits","github_compare_commits_v2","github_create_branch","github_create_branch_v2","github_create_comment_reaction","github_create_comment_reaction_v2","github_create_file","github_create_file_v2","github_create_gist","github_create_gist_v2","github_create_issue","github_create_issue_reaction","github_create_issue_reaction_v2","github_create_issue_v2","github_create_milestone","github_create_milestone_v2","github_create_pr","github_create_pr_review","github_create_pr_review_v2","github_create_pr_v2","github_create_project","github_create_project_v2","github_create_release","github_create_release_v2","github_delete_branch","github_delete_branch_v2","github_delete_comment","github_delete_comment_reaction","github_delete_comment_reaction_v2","github_delete_comment_v2","github_delete_file","github_delete_file_v2","github_delete_gist","github_delete_gist_v2","github_delete_issue_reaction","github_delete_issue_reaction_v2","github_delete_milestone","github_delete_milestone_v2","github_delete_project","github_delete_project_v2","github_delete_release","github_delete_release_v2","github_fork_gist","github_fork_gist_v2","github_fork_repo","github_fork_repo_v2","github_get_branch","github_get_branch_protection","github_get_branch_protection_v2","github_get_branch_v2","github_get_commit","github_get_commit_v2","github_get_file_content","github_get_file_content_v2","github_get_gist","github_get_gist_v2","github_get_issue","github_get_issue_v2","github_get_latest_release","github_get_latest_release_v2","github_get_milestone","github_get_milestone_v2","github_get_pr_files","github_get_pr_files_v2","github_get_project","github_get_project_v2","github_get_readme","github_get_readme_v2","github_get_release","github_get_release_v2","github_get_tree","github_get_tree_v2","github_get_workflow","github_get_workflow_run","github_get_workflow_run_v2","github_get_workflow_v2","github_issue_comment","github_issue_comment_v2","github_job_logs","github_latest_commit","github_latest_commit_v2","github_list_branches","github_list_branches_v2","github_list_commits","github_list_commits_v2","github_list_forks","github_list_forks_v2","github_list_gists","github_list_gists_v2","github_list_issue_comments","github_list_issue_comments_v2","github_list_issues","github_list_issues_v2","github_list_milestones","github_list_milestones_v2","github_list_pr_comments","github_list_pr_comments_v2","github_list_projects","github_list_projects_v2","github_list_prs","github_list_prs_v2","github_list_releases","github_list_releases_v2","github_list_review_threads","github_list_stargazers","github_list_stargazers_v2","github_list_tags","github_list_tags_v2","github_list_workflow_runs","github_list_workflow_runs_v2","github_list_workflows","github_list_workflows_v2","github_merge_pr","github_merge_pr_v2","github_pr","github_pr_v2","github_remove_label","github_remove_label_v2","github_reply_review_thread","github_repo_info","github_repo_info_v2","github_request_reviewers","github_request_reviewers_v2","github_rerun_workflow","github_rerun_workflow_v2","github_resolve_review_thread","github_search_code","github_search_code_v2","github_search_commits","github_search_commits_v2","github_search_issues","github_search_issues_v2","github_search_repos","github_search_repos_v2","github_search_users","github_search_users_v2","github_star_gist","github_star_gist_v2","github_star_repo","github_star_repo_v2","github_status_check_rollup","github_trigger_workflow","github_trigger_workflow_v2","github_unstar_gist","github_unstar_gist_v2","github_unstar_repo","github_unstar_repo_v2","github_update_branch_protection","github_update_branch_protection_v2","github_update_comment","github_update_comment_v2","github_update_file","github_update_file_v2","github_update_gist","github_update_gist_v2","github_update_issue","github_update_issue_v2","github_update_milestone","github_update_milestone_v2","github_update_pr","github_update_pr_v2","github_update_project","github_update_project_v2","github_update_release","github_update_release_v2","gitlab_activate_user","gitlab_add_member","gitlab_add_saml_group_link","gitlab_approve_access_request","gitlab_approve_merge_request","gitlab_approve_user","gitlab_ban_user","gitlab_block_user","gitlab_cancel_pipeline","gitlab_compare_branches","gitlab_create_branch","gitlab_create_file","gitlab_create_issue","gitlab_create_issue_note","gitlab_create_merge_request","gitlab_create_merge_request_note","gitlab_create_pipeline","gitlab_create_release","gitlab_create_user","gitlab_deactivate_user","gitlab_delete_branch","gitlab_delete_issue","gitlab_delete_saml_group_link","gitlab_delete_user","gitlab_delete_user_identity","gitlab_deny_access_request","gitlab_get_file","gitlab_get_group","gitlab_get_issue","gitlab_get_job_log","gitlab_get_merge_request","gitlab_get_merge_request_changes","gitlab_get_pipeline","gitlab_get_project","gitlab_invite_member","gitlab_list_access_requests","gitlab_list_branches","gitlab_list_commits","gitlab_list_groups","gitlab_list_invitations","gitlab_list_issues","gitlab_list_members","gitlab_list_merge_requests","gitlab_list_pipeline_jobs","gitlab_list_pipelines","gitlab_list_projects","gitlab_list_releases","gitlab_list_repository_tree","gitlab_list_saml_group_links","gitlab_list_user_memberships","gitlab_merge_merge_request","gitlab_play_job","gitlab_reject_user","gitlab_remove_member","gitlab_retry_pipeline","gitlab_revoke_invitation","gitlab_search_users","gitlab_unban_user","gitlab_unblock_user","gitlab_update_file","gitlab_update_invitation","gitlab_update_issue","gitlab_update_member","gitlab_update_merge_request","gitlab_update_user","gmail_add_label","gmail_add_label_v2","gmail_archive","gmail_archive_v2","gmail_create_label_v2","gmail_delete","gmail_delete_draft_v2","gmail_delete_label_v2","gmail_delete_v2","gmail_draft","gmail_draft_v2","gmail_edit_draft_v2","gmail_get_draft_v2","gmail_get_thread_v2","gmail_list_drafts_v2","gmail_list_labels_v2","gmail_list_threads_v2","gmail_mark_read","gmail_mark_read_v2","gmail_mark_unread","gmail_mark_unread_v2","gmail_move","gmail_move_v2","gmail_read","gmail_read_v2","gmail_remove_label","gmail_remove_label_v2","gmail_search","gmail_search_v2","gmail_send","gmail_send_v2","gmail_trash_thread_v2","gmail_unarchive","gmail_unarchive_v2","gmail_untrash_thread_v2","gmail_update_label_v2","gong_aggregate_activity","gong_aggregate_by_period","gong_answered_scorecards","gong_ask_anything","gong_assign_flow_prospects","gong_create_call","gong_day_by_day_activity","gong_get_brief","gong_get_call","gong_get_call_transcript","gong_get_coaching","gong_get_extensive_calls","gong_get_folder_content","gong_get_logs","gong_get_prospect_flows","gong_get_user","gong_interaction_stats","gong_list_calls","gong_list_flows","gong_list_library_folders","gong_list_scorecards","gong_list_trackers","gong_list_users","gong_list_workspaces","gong_lookup_email","gong_lookup_phone","gong_purge_email_address","gong_purge_phone_number","gong_unassign_flow_prospects","google_ads_ad_performance","google_ads_campaign_performance","google_ads_list_ad_groups","google_ads_list_campaigns","google_ads_list_customers","google_ads_search","google_appsheet_add_rows","google_appsheet_delete_rows","google_appsheet_edit_rows","google_appsheet_find_rows","google_bigquery_create_dataset","google_bigquery_create_table","google_bigquery_delete_dataset","google_bigquery_delete_table","google_bigquery_get_query_results","google_bigquery_get_table","google_bigquery_insert_rows","google_bigquery_list_datasets","google_bigquery_list_table_data","google_bigquery_list_tables","google_bigquery_query","google_books_volume_details","google_books_volume_search","google_calendar_create","google_calendar_create_calendar","google_calendar_create_calendar_v2","google_calendar_create_v2","google_calendar_delete","google_calendar_delete_calendar","google_calendar_delete_calendar_v2","google_calendar_delete_v2","google_calendar_freebusy","google_calendar_freebusy_v2","google_calendar_get","google_calendar_get_v2","google_calendar_instances","google_calendar_instances_v2","google_calendar_invite","google_calendar_invite_v2","google_calendar_list","google_calendar_list_acl","google_calendar_list_acl_v2","google_calendar_list_calendars","google_calendar_list_calendars_v2","google_calendar_list_v2","google_calendar_move","google_calendar_move_v2","google_calendar_quick_add","google_calendar_quick_add_v2","google_calendar_share_calendar","google_calendar_share_calendar_v2","google_calendar_unshare_calendar","google_calendar_unshare_calendar_v2","google_calendar_update","google_calendar_update_acl","google_calendar_update_acl_v2","google_calendar_update_calendar","google_calendar_update_calendar_v2","google_calendar_update_v2","google_contacts_create","google_contacts_delete","google_contacts_get","google_contacts_list","google_contacts_search","google_contacts_update","google_docs_create","google_docs_create_named_range","google_docs_create_paragraph_bullets","google_docs_delete_content_range","google_docs_delete_named_range","google_docs_delete_paragraph_bullets","google_docs_insert_image","google_docs_insert_page_break","google_docs_insert_table","google_docs_insert_text","google_docs_read","google_docs_replace_text","google_docs_update_paragraph_style","google_docs_update_text_style","google_docs_write","google_drive_copy","google_drive_create_comment","google_drive_create_folder","google_drive_delete","google_drive_delete_comment","google_drive_download","google_drive_export","google_drive_get_about","google_drive_get_content","google_drive_get_file","google_drive_get_revision","google_drive_list","google_drive_list_comments","google_drive_list_permissions","google_drive_list_revisions","google_drive_move","google_drive_search","google_drive_share","google_drive_trash","google_drive_unshare","google_drive_untrash","google_drive_update","google_drive_upload","google_forms_batch_update","google_forms_create_form","google_forms_create_watch","google_forms_delete_watch","google_forms_get_form","google_forms_get_responses","google_forms_list_watches","google_forms_renew_watch","google_forms_set_publish_settings","google_groups_add_alias","google_groups_add_member","google_groups_create_group","google_groups_delete_group","google_groups_get_group","google_groups_get_member","google_groups_get_settings","google_groups_has_member","google_groups_list_aliases","google_groups_list_groups","google_groups_list_members","google_groups_remove_alias","google_groups_remove_member","google_groups_update_group","google_groups_update_member","google_groups_update_settings","google_maps_air_quality","google_maps_directions","google_maps_distance_matrix","google_maps_elevation","google_maps_geocode","google_maps_geolocate","google_maps_place_details","google_maps_places_nearby","google_maps_places_search","google_maps_pollen","google_maps_reverse_geocode","google_maps_snap_to_roads","google_maps_solar","google_maps_speed_limits","google_maps_timezone","google_maps_validate_address","google_meet_create_space","google_meet_end_conference","google_meet_get_conference_record","google_meet_get_space","google_meet_list_conference_records","google_meet_list_participants","google_pagespeed_analyze","google_search","google_sheets_append","google_sheets_append_v2","google_sheets_batch_clear_v2","google_sheets_batch_get_v2","google_sheets_batch_update_v2","google_sheets_clear_v2","google_sheets_copy_sheet_v2","google_sheets_create_spreadsheet_v2","google_sheets_delete_rows_v2","google_sheets_delete_sheet_v2","google_sheets_delete_spreadsheet_v2","google_sheets_get_spreadsheet_v2","google_sheets_read","google_sheets_read_v2","google_sheets_update","google_sheets_update_v2","google_sheets_write","google_sheets_write_v2","google_slides_add_image","google_slides_add_slide","google_slides_batch_update","google_slides_copy_presentation","google_slides_create","google_slides_create_line","google_slides_create_paragraph_bullets","google_slides_create_shape","google_slides_create_sheets_chart","google_slides_create_table","google_slides_create_video","google_slides_delete_object","google_slides_delete_paragraph_bullets","google_slides_delete_table_column","google_slides_delete_table_row","google_slides_delete_text","google_slides_duplicate_object","google_slides_export_presentation","google_slides_get_page","google_slides_get_thumbnail","google_slides_group_objects","google_slides_insert_table_columns","google_slides_insert_table_rows","google_slides_insert_text","google_slides_merge_table_cells","google_slides_read","google_slides_refresh_sheets_chart","google_slides_replace_all_shapes_with_image","google_slides_replace_all_shapes_with_sheets_chart","google_slides_replace_all_text","google_slides_replace_image","google_slides_reroute_line","google_slides_ungroup_objects","google_slides_unmerge_table_cells","google_slides_update_image_properties","google_slides_update_line_category","google_slides_update_line_properties","google_slides_update_page_element_alt_text","google_slides_update_page_element_transform","google_slides_update_page_elements_z_order","google_slides_update_page_properties","google_slides_update_paragraph_style","google_slides_update_shape_properties","google_slides_update_slide_properties","google_slides_update_slides_position","google_slides_update_table_border_properties","google_slides_update_table_cell_properties","google_slides_update_table_column_properties","google_slides_update_table_row_properties","google_slides_update_text_style","google_slides_update_video_properties","google_slides_write","google_tasks_create","google_tasks_delete","google_tasks_get","google_tasks_list","google_tasks_list_task_lists","google_tasks_update","google_translate_detect","google_translate_text","google_vault_add_held_accounts","google_vault_add_matters_permissions","google_vault_close_matters","google_vault_create_matters","google_vault_create_matters_export","google_vault_create_matters_holds","google_vault_create_saved_query","google_vault_delete_matters","google_vault_delete_matters_export","google_vault_delete_matters_holds","google_vault_delete_saved_query","google_vault_download_export_file","google_vault_list_matters","google_vault_list_matters_export","google_vault_list_matters_holds","google_vault_list_saved_queries","google_vault_remove_held_accounts","google_vault_remove_matters_permissions","google_vault_reopen_matters","google_vault_undelete_matters","google_vault_update_matters","google_vault_update_matters_holds","grafana_check_data_source_health","grafana_create_alert_rule","grafana_create_annotation","grafana_create_contact_point","grafana_create_dashboard","grafana_create_folder","grafana_delete_alert_rule","grafana_delete_annotation","grafana_delete_contact_point","grafana_delete_dashboard","grafana_delete_folder","grafana_get_alert_rule","grafana_get_alert_rule_group","grafana_get_dashboard","grafana_get_data_source","grafana_get_folder","grafana_get_health","grafana_list_alert_rules","grafana_list_annotations","grafana_list_contact_points","grafana_list_dashboards","grafana_list_data_sources","grafana_list_folders","grafana_move_folder","grafana_query_data_source","grafana_update_alert_rule","grafana_update_annotation","grafana_update_contact_point","grafana_update_dashboard","grafana_update_folder","grain_create_hook","grain_create_hook_v2","grain_delete_hook","grain_delete_hook_v2","grain_get_recording","grain_get_transcript","grain_list_hooks","grain_list_hooks_v2","grain_list_meeting_types","grain_list_recordings","grain_list_teams","grain_list_views","granola_create_webhook_endpoint","granola_delete_webhook_endpoint","granola_get_note","granola_get_transcript","granola_list_audit_events","granola_list_folders","granola_list_notes","granola_list_webhook_endpoints","granola_update_webhook_endpoint","greenhouse_get_application","greenhouse_get_candidate","greenhouse_get_job","greenhouse_get_user","greenhouse_list_applications","greenhouse_list_candidates","greenhouse_list_departments","greenhouse_list_job_stages","greenhouse_list_jobs","greenhouse_list_offices","greenhouse_list_users","greptile_index_repo","greptile_query","greptile_search","greptile_status","guardrails_validate","harmonic_batch_get_people","harmonic_clear_people_saved_search_net_new_results","harmonic_enrich_person","harmonic_get_company_employees","harmonic_get_email_enrichment_job","harmonic_get_email_enrichment_usage","harmonic_get_enrichment_status","harmonic_get_people_saved_search_net_new_results","harmonic_get_people_saved_search_results","harmonic_get_person","harmonic_list_people_saved_searches","harmonic_search_people_scout","harmonic_submit_email_enrichment_job","hex_cancel_run","hex_create_collection","hex_create_group","hex_deactivate_user","hex_delete_group","hex_get_collection","hex_get_data_connection","hex_get_group","hex_get_project","hex_get_project_runs","hex_get_queried_tables","hex_get_run_status","hex_list_collections","hex_list_data_connections","hex_list_groups","hex_list_projects","hex_list_users","hex_run_project","hex_update_collection","hex_update_group","hex_update_project","http_request","hubspot_add_list_memberships","hubspot_create_appointment","hubspot_create_association","hubspot_create_company","hubspot_create_contact","hubspot_create_deal","hubspot_create_email","hubspot_create_line_item","hubspot_create_list","hubspot_create_note","hubspot_create_ticket","hubspot_delete_association","hubspot_delete_company","hubspot_delete_contact","hubspot_delete_deal","hubspot_delete_line_item","hubspot_delete_ticket","hubspot_get_appointment","hubspot_get_association_labels","hubspot_get_cart","hubspot_get_company","hubspot_get_contact","hubspot_get_deal","hubspot_get_email","hubspot_get_line_item","hubspot_get_list","hubspot_get_list_memberships","hubspot_get_marketing_event","hubspot_get_note","hubspot_get_properties","hubspot_get_quote","hubspot_get_ticket","hubspot_get_users","hubspot_list_appointments","hubspot_list_associations","hubspot_list_carts","hubspot_list_companies","hubspot_list_contacts","hubspot_list_deals","hubspot_list_emails","hubspot_list_line_items","hubspot_list_lists","hubspot_list_marketing_events","hubspot_list_notes","hubspot_list_owners","hubspot_list_quotes","hubspot_list_tickets","hubspot_remove_list_memberships","hubspot_search_companies","hubspot_search_contacts","hubspot_search_deals","hubspot_search_emails","hubspot_search_line_items","hubspot_search_notes","hubspot_search_quotes","hubspot_search_tickets","hubspot_update_appointment","hubspot_update_company","hubspot_update_contact","hubspot_update_deal","hubspot_update_line_item","hubspot_update_ticket","huggingface_chat","hunter_companies_find","hunter_discover","hunter_domain_search","hunter_email_count","hunter_email_finder","hunter_email_verifier","iam_add_user_to_group","iam_attach_role_policy","iam_attach_user_policy","iam_create_access_key","iam_create_role","iam_create_user","iam_delete_access_key","iam_delete_role","iam_delete_user","iam_detach_role_policy","iam_detach_user_policy","iam_get_role","iam_get_user","iam_list_attached_role_policies","iam_list_attached_user_policies","iam_list_groups","iam_list_policies","iam_list_roles","iam_list_users","iam_remove_user_from_group","iam_simulate_principal_policy","icypeas_find_email","icypeas_verify_email","identity_center_check_assignment_deletion_status","identity_center_check_assignment_status","identity_center_create_account_assignment","identity_center_delete_account_assignment","identity_center_describe_account","identity_center_get_group","identity_center_get_user","identity_center_list_account_assignments","identity_center_list_accounts","identity_center_list_groups","identity_center_list_instances","identity_center_list_permission_sets","image_generate","incidentio_actions_create","incidentio_actions_list","incidentio_actions_show","incidentio_actions_update","incidentio_alert_events_create","incidentio_alerts_list","incidentio_alerts_resolve","incidentio_alerts_show","incidentio_catalog_entries_list","incidentio_catalog_types_list","incidentio_custom_fields_create","incidentio_custom_fields_delete","incidentio_custom_fields_list","incidentio_custom_fields_show","incidentio_custom_fields_update","incidentio_escalation_paths_create","incidentio_escalation_paths_delete","incidentio_escalation_paths_list","incidentio_escalation_paths_show","incidentio_escalation_paths_update","incidentio_escalations_cancel","incidentio_escalations_create","incidentio_escalations_list","incidentio_escalations_show","incidentio_follow_ups_create","incidentio_follow_ups_list","incidentio_follow_ups_show","incidentio_follow_ups_update","incidentio_incident_alerts_list","incidentio_incident_memberships_create","incidentio_incident_memberships_revoke","incidentio_incident_participants_list","incidentio_incident_roles_create","incidentio_incident_roles_delete","incidentio_incident_roles_list","incidentio_incident_roles_show","incidentio_incident_roles_update","incidentio_incident_statuses_list","incidentio_incident_timestamps_list","incidentio_incident_timestamps_show","incidentio_incident_types_list","incidentio_incident_updates_list","incidentio_incidents_create","incidentio_incidents_list","incidentio_incidents_show","incidentio_incidents_update","incidentio_on_call_now","incidentio_schedule_entries_list","incidentio_schedule_overrides_create","incidentio_schedule_overrides_list","incidentio_schedules_create","incidentio_schedules_delete","incidentio_schedules_list","incidentio_schedules_show","incidentio_schedules_update","incidentio_severities_list","incidentio_teams_list","incidentio_teams_show","incidentio_users_list","incidentio_users_show","incidentio_workflows_create","incidentio_workflows_delete","incidentio_workflows_list","incidentio_workflows_show","incidentio_workflows_update","infisical_create_secret","infisical_delete_secret","infisical_get_secret","infisical_list_secrets","infisical_update_secret","instagram_delete_comment","instagram_download_media","instagram_get_account_insights","instagram_get_container_status","instagram_get_conversation_messages","instagram_get_media","instagram_get_media_insights","instagram_get_message","instagram_get_profile","instagram_get_publishing_limit","instagram_hide_comment","instagram_list_comments","instagram_list_conversations","instagram_list_media","instagram_list_stories","instagram_private_reply","instagram_publish_carousel","instagram_publish_image","instagram_publish_reel","instagram_publish_story","instagram_publish_video","instagram_reply_to_comment","instagram_send_text_message","instagram_set_comments_enabled","instantly_activate_campaign","instantly_create_campaign","instantly_create_lead","instantly_create_lead_list","instantly_delete_campaign","instantly_delete_leads","instantly_get_lead","instantly_list_campaigns","instantly_list_emails","instantly_list_lead_lists","instantly_list_leads","instantly_patch_campaign","instantly_patch_lead","instantly_pause_campaign","instantly_reply_to_email","instantly_update_lead_interest_status","intercom_assign_conversation_v2","intercom_attach_contact_to_company_v2","intercom_close_conversation_v2","intercom_create_company","intercom_create_company_v2","intercom_create_contact","intercom_create_contact_v2","intercom_create_event_v2","intercom_create_message","intercom_create_message_v2","intercom_create_note_v2","intercom_create_tag_v2","intercom_create_ticket","intercom_create_ticket_v2","intercom_delete_contact","intercom_delete_contact_v2","intercom_detach_contact_from_company_v2","intercom_get_company","intercom_get_company_v2","intercom_get_contact","intercom_get_contact_v2","intercom_get_conversation","intercom_get_conversation_v2","intercom_get_ticket","intercom_get_ticket_v2","intercom_list_admins_v2","intercom_list_companies","intercom_list_companies_v2","intercom_list_contacts","intercom_list_contacts_v2","intercom_list_conversations","intercom_list_conversations_v2","intercom_list_tags_v2","intercom_open_conversation_v2","intercom_reply_conversation","intercom_reply_conversation_v2","intercom_search_contacts","intercom_search_contacts_v2","intercom_search_conversations","intercom_search_conversations_v2","intercom_snooze_conversation_v2","intercom_tag_contact_v2","intercom_tag_conversation_v2","intercom_untag_contact_v2","intercom_update_contact","intercom_update_contact_v2","intercom_update_ticket_v2","jina_read_url","jina_search","jira_add_attachment","jira_add_comment","jira_add_watcher","jira_add_worklog","jira_assign_issue","jira_bulk_read","jira_create_issue_link","jira_delete_attachment","jira_delete_comment","jira_delete_issue","jira_delete_issue_link","jira_delete_worklog","jira_get_attachments","jira_get_comments","jira_get_fields","jira_get_project","jira_get_transitions","jira_get_users","jira_get_worklogs","jira_list_issue_types","jira_list_projects","jira_remove_watcher","jira_retrieve","jira_search_issues","jira_search_users","jira_transition_issue","jira_update","jira_update_comment","jira_update_worklog","jira_write","jotform_add_label_resources","jotform_clone_form","jotform_create_form","jotform_create_label","jotform_create_question","jotform_create_questions","jotform_create_report","jotform_create_submission","jotform_create_submissions","jotform_create_webhook","jotform_delete_form","jotform_delete_label","jotform_delete_question","jotform_delete_report","jotform_delete_submission","jotform_delete_webhook","jotform_get_form","jotform_get_form_properties","jotform_get_history","jotform_get_label","jotform_get_question","jotform_get_report","jotform_get_settings","jotform_get_submission","jotform_get_usage","jotform_get_user","jotform_list_form_files","jotform_list_form_reports","jotform_list_form_submissions","jotform_list_forms","jotform_list_label_resources","jotform_list_labels","jotform_list_questions","jotform_list_reports","jotform_list_submissions","jotform_list_subusers","jotform_list_webhooks","jotform_remove_label_resources","jotform_update_form_properties","jotform_update_label","jotform_update_question","jotform_update_settings","jotform_update_submission","jsm_add_comment","jsm_add_customer","jsm_add_organization","jsm_add_participants","jsm_answer_approval","jsm_attach_form","jsm_copy_forms","jsm_create_object","jsm_create_organization","jsm_create_request","jsm_delete_form","jsm_delete_object","jsm_externalise_form","jsm_get_approvals","jsm_get_comments","jsm_get_customers","jsm_get_form","jsm_get_form_answers","jsm_get_form_structure","jsm_get_form_templates","jsm_get_issue_forms","jsm_get_object","jsm_get_object_schema","jsm_get_object_type_attributes","jsm_get_organizations","jsm_get_participants","jsm_get_queues","jsm_get_request","jsm_get_request_type_fields","jsm_get_request_types","jsm_get_requests","jsm_get_service_desks","jsm_get_sla","jsm_get_transitions","jsm_internalise_form","jsm_list_object_schemas","jsm_list_object_types","jsm_reopen_form","jsm_save_form_answers","jsm_search_objects_aql","jsm_submit_form","jsm_transition_request","jsm_update_object","jupyter_copy_content","jupyter_create_file","jupyter_create_session","jupyter_delete_content","jupyter_delete_session","jupyter_get_content","jupyter_interrupt_kernel","jupyter_list_contents","jupyter_list_kernels","jupyter_list_kernelspecs","jupyter_list_sessions","jupyter_rename_content","jupyter_restart_kernel","jupyter_start_kernel","jupyter_stop_kernel","jupyter_upload_file","kalshi_amend_order","kalshi_amend_order_v2","kalshi_cancel_order","kalshi_cancel_order_v2","kalshi_create_order","kalshi_create_order_v2","kalshi_get_balance","kalshi_get_balance_v2","kalshi_get_candlesticks","kalshi_get_candlesticks_v2","kalshi_get_event","kalshi_get_event_candlesticks","kalshi_get_event_candlesticks_v2","kalshi_get_event_v2","kalshi_get_events","kalshi_get_events_v2","kalshi_get_exchange_announcements","kalshi_get_exchange_announcements_v2","kalshi_get_exchange_schedule","kalshi_get_exchange_schedule_v2","kalshi_get_exchange_status","kalshi_get_exchange_status_v2","kalshi_get_fills","kalshi_get_fills_v2","kalshi_get_market","kalshi_get_market_v2","kalshi_get_markets","kalshi_get_markets_v2","kalshi_get_order","kalshi_get_order_v2","kalshi_get_orderbook","kalshi_get_orderbook_v2","kalshi_get_orders","kalshi_get_orders_v2","kalshi_get_positions","kalshi_get_positions_v2","kalshi_get_series_by_ticker","kalshi_get_series_by_ticker_v2","kalshi_get_series_list","kalshi_get_series_list_v2","kalshi_get_settlements","kalshi_get_settlements_v2","kalshi_get_trades","kalshi_get_trades_v2","ketch_get_consent","ketch_get_subscriptions","ketch_invoke_right","ketch_set_consent","ketch_set_subscriptions","knowledge_create_document","knowledge_delete_chunk","knowledge_delete_document","knowledge_get_connector","knowledge_get_document","knowledge_list_chunks","knowledge_list_connectors","knowledge_list_documents","knowledge_list_tags","knowledge_search","knowledge_trigger_sync","knowledge_update_chunk","knowledge_upload_chunk","knowledge_upsert_document","lambda_add_permission","lambda_create_alias","lambda_create_event_source_mapping","lambda_create_function","lambda_create_function_url_config","lambda_delete_alias","lambda_delete_event_source_mapping","lambda_delete_function","lambda_delete_function_concurrency","lambda_delete_function_event_invoke_config","lambda_delete_function_url_config","lambda_delete_provisioned_concurrency_config","lambda_get_account_settings","lambda_get_alias","lambda_get_event_source_mapping","lambda_get_function","lambda_get_function_concurrency","lambda_get_function_configuration","lambda_get_function_event_invoke_config","lambda_get_function_recursion_config","lambda_get_function_url_config","lambda_get_layer_version","lambda_get_policy","lambda_get_provisioned_concurrency_config","lambda_get_runtime_management_config","lambda_invoke","lambda_list_aliases","lambda_list_event_source_mappings","lambda_list_function_event_invoke_configs","lambda_list_function_url_configs","lambda_list_functions","lambda_list_layer_versions","lambda_list_layers","lambda_list_provisioned_concurrency_configs","lambda_list_tags","lambda_list_versions_by_function","lambda_publish_version","lambda_put_function_concurrency","lambda_put_function_event_invoke_config","lambda_put_function_recursion_config","lambda_put_provisioned_concurrency_config","lambda_put_runtime_management_config","lambda_remove_permission","lambda_tag_resource","lambda_untag_resource","lambda_update_alias","lambda_update_event_source_mapping","lambda_update_function_code","lambda_update_function_configuration","lambda_update_function_url_config","langsmith_create_feedback","langsmith_create_run","langsmith_create_runs_batch","langsmith_get_run","langsmith_update_run","latex_compile","latex_get_package","latex_list_fonts","latex_search_packages","launchdarkly_create_flag","launchdarkly_delete_flag","launchdarkly_get_audit_log","launchdarkly_get_flag","launchdarkly_get_flag_status","launchdarkly_list_environments","launchdarkly_list_flags","launchdarkly_list_members","launchdarkly_list_projects","launchdarkly_list_segments","launchdarkly_toggle_flag","launchdarkly_update_flag","leadmagic_company_search","leadmagic_email_to_profile","leadmagic_find_email","leadmagic_find_mobile","leadmagic_get_credits","leadmagic_profile_search","leadmagic_profile_to_email","leadmagic_role_finder","leadmagic_validate_email","lemlist_get_activities","lemlist_get_lead","lemlist_send_email","linear_add_label_to_issue","linear_add_label_to_project","linear_archive_issue","linear_archive_label","linear_archive_project","linear_create_attachment","linear_create_comment","linear_create_customer","linear_create_customer_request","linear_create_customer_status","linear_create_customer_tier","linear_create_cycle","linear_create_favorite","linear_create_issue","linear_create_issue_relation","linear_create_label","linear_create_project","linear_create_project_label","linear_create_project_milestone","linear_create_project_status","linear_create_project_update","linear_create_workflow_state","linear_delete_attachment","linear_delete_comment","linear_delete_customer","linear_delete_customer_status","linear_delete_customer_tier","linear_delete_issue","linear_delete_issue_relation","linear_delete_project","linear_delete_project_label","linear_delete_project_milestone","linear_delete_project_status","linear_get_active_cycle","linear_get_customer","linear_get_cycle","linear_get_issue","linear_get_project","linear_get_viewer","linear_list_attachments","linear_list_comments","linear_list_customer_requests","linear_list_customer_statuses","linear_list_customer_tiers","linear_list_customers","linear_list_cycles","linear_list_favorites","linear_list_issue_relations","linear_list_labels","linear_list_notifications","linear_list_project_labels","linear_list_project_milestones","linear_list_project_statuses","linear_list_project_updates","linear_list_projects","linear_list_teams","linear_list_users","linear_list_workflow_states","linear_merge_customers","linear_read_issues","linear_remove_label_from_issue","linear_remove_label_from_project","linear_search_issues","linear_unarchive_issue","linear_update_attachment","linear_update_comment","linear_update_customer","linear_update_customer_request","linear_update_customer_status","linear_update_customer_tier","linear_update_issue","linear_update_label","linear_update_notification","linear_update_project","linear_update_project_label","linear_update_project_milestone","linear_update_project_status","linear_update_workflow_state","linkedin_get_profile","linkedin_share_post","linkup_search","linq_add_participant","linq_check_imessage","linq_check_rcs","linq_create_attachment","linq_create_chat","linq_create_contact_card","linq_create_webhook_subscription","linq_delete_attachment","linq_delete_message","linq_delete_webhook_subscription","linq_edit_message","linq_get_attachment","linq_get_chat","linq_get_contact_card","linq_get_message","linq_get_webhook_subscription","linq_leave_chat","linq_list_chats","linq_list_messages","linq_list_phone_numbers","linq_list_thread","linq_list_webhook_events","linq_list_webhook_subscriptions","linq_mark_chat_read","linq_react_to_message","linq_remove_participant","linq_send_message","linq_send_voice_memo","linq_share_contact_card","linq_start_typing","linq_stop_typing","linq_update_chat","linq_update_contact_card","linq_update_webhook_subscription","llm_chat","logfire_get_token_info","logfire_get_trace","logfire_query","logfire_search_records","logrocket_create_release","logrocket_get_audit_logs","logrocket_get_highlights","logrocket_identify_user","logrocket_list_exported_sessions","logrocket_request_highlights","logs_get","logs_get_execution","logs_get_run_details","logs_query","logs_query_runs","loops_check_contact_suppression","loops_create_contact","loops_create_contact_property","loops_delete_contact","loops_find_contact","loops_get_transactional_email","loops_list_contact_properties","loops_list_mailing_lists","loops_list_transactional_emails","loops_remove_contact_suppression","loops_send_event","loops_send_transactional_email","loops_update_contact","luma_add_guests","luma_cancel_event","luma_create_event","luma_get_event","luma_get_guest","luma_get_guests","luma_list_events","luma_lookup_event","luma_send_invites","luma_update_event","luma_update_guest_status","mailchimp_add_member","mailchimp_add_member_tags","mailchimp_add_or_update_member","mailchimp_add_segment_member","mailchimp_add_subscriber_to_automation","mailchimp_archive_member","mailchimp_create_audience","mailchimp_create_batch_operation","mailchimp_create_campaign","mailchimp_create_interest","mailchimp_create_interest_category","mailchimp_create_landing_page","mailchimp_create_merge_field","mailchimp_create_segment","mailchimp_create_template","mailchimp_delete_audience","mailchimp_delete_batch_operation","mailchimp_delete_campaign","mailchimp_delete_interest","mailchimp_delete_interest_category","mailchimp_delete_landing_page","mailchimp_delete_member","mailchimp_delete_merge_field","mailchimp_delete_segment","mailchimp_delete_template","mailchimp_get_audience","mailchimp_get_audiences","mailchimp_get_automation","mailchimp_get_automations","mailchimp_get_batch_operation","mailchimp_get_batch_operations","mailchimp_get_campaign","mailchimp_get_campaign_content","mailchimp_get_campaign_report","mailchimp_get_campaign_reports","mailchimp_get_campaigns","mailchimp_get_interest","mailchimp_get_interest_categories","mailchimp_get_interest_category","mailchimp_get_interests","mailchimp_get_landing_page","mailchimp_get_landing_pages","mailchimp_get_member","mailchimp_get_member_tags","mailchimp_get_members","mailchimp_get_merge_field","mailchimp_get_merge_fields","mailchimp_get_segment","mailchimp_get_segment_members","mailchimp_get_segments","mailchimp_get_template","mailchimp_get_templates","mailchimp_pause_automation","mailchimp_publish_landing_page","mailchimp_remove_member_tags","mailchimp_remove_segment_member","mailchimp_replicate_campaign","mailchimp_schedule_campaign","mailchimp_send_campaign","mailchimp_set_campaign_content","mailchimp_start_automation","mailchimp_unarchive_member","mailchimp_unpublish_landing_page","mailchimp_unschedule_campaign","mailchimp_update_audience","mailchimp_update_campaign","mailchimp_update_interest","mailchimp_update_interest_category","mailchimp_update_landing_page","mailchimp_update_member","mailchimp_update_merge_field","mailchimp_update_segment","mailchimp_update_template","mailgun_add_list_member","mailgun_create_mailing_list","mailgun_get_domain","mailgun_get_mailing_list","mailgun_get_message","mailgun_list_domains","mailgun_list_messages","mailgun_send_message","managed_agent_archive_session","managed_agent_create_session","managed_agent_delete_session","managed_agent_get_session","managed_agent_interrupt_session","managed_agent_list_events","managed_agent_respond_custom_tool","managed_agent_respond_tool_confirmation","managed_agent_run_session","managed_agent_send_message","managed_agent_update_session","manageengine_sdp_add_change_note","manageengine_sdp_add_problem_note","manageengine_sdp_add_request_note","manageengine_sdp_create_asset","manageengine_sdp_create_change","manageengine_sdp_create_problem","manageengine_sdp_create_request","manageengine_sdp_create_solution","manageengine_sdp_delete_asset","manageengine_sdp_delete_change","manageengine_sdp_delete_problem","manageengine_sdp_delete_request","manageengine_sdp_delete_solution","manageengine_sdp_get_asset","manageengine_sdp_get_change","manageengine_sdp_get_problem","manageengine_sdp_get_request","manageengine_sdp_get_solution","manageengine_sdp_list_assets","manageengine_sdp_list_change_notes","manageengine_sdp_list_changes","manageengine_sdp_list_problem_notes","manageengine_sdp_list_problems","manageengine_sdp_list_request_notes","manageengine_sdp_list_requests","manageengine_sdp_list_solutions","manageengine_sdp_update_asset","manageengine_sdp_update_change","manageengine_sdp_update_problem","manageengine_sdp_update_request","manageengine_sdp_update_solution","mem0_add_memories","mem0_get_memories","mem0_search_memories","memory_add","memory_delete","memory_get","memory_get_all","microsoft_ad_add_directory_role_member","microsoft_ad_add_group_member","microsoft_ad_add_user_app_role_assignment","microsoft_ad_assign_license","microsoft_ad_create_group","microsoft_ad_create_user","microsoft_ad_delete_group","microsoft_ad_delete_user","microsoft_ad_get_conditional_access_policy","microsoft_ad_get_device","microsoft_ad_get_group","microsoft_ad_get_user","microsoft_ad_list_authentication_methods","microsoft_ad_list_conditional_access_policies","microsoft_ad_list_devices","microsoft_ad_list_directory_audits","microsoft_ad_list_directory_role_members","microsoft_ad_list_directory_roles","microsoft_ad_list_group_members","microsoft_ad_list_groups","microsoft_ad_list_service_principal_app_role_assignments","microsoft_ad_list_service_principals","microsoft_ad_list_sign_ins","microsoft_ad_list_subscribed_skus","microsoft_ad_list_user_app_role_assignments","microsoft_ad_list_user_devices","microsoft_ad_list_user_licenses","microsoft_ad_list_users","microsoft_ad_remove_directory_role_member","microsoft_ad_remove_group_member","microsoft_ad_remove_user_app_role_assignment","microsoft_ad_reset_password","microsoft_ad_revoke_sign_in_sessions","microsoft_ad_set_password","microsoft_ad_update_group","microsoft_ad_update_user","microsoft_dataverse_associate","microsoft_dataverse_create_multiple","microsoft_dataverse_create_record","microsoft_dataverse_delete_record","microsoft_dataverse_disassociate","microsoft_dataverse_download_file","microsoft_dataverse_execute_action","microsoft_dataverse_execute_function","microsoft_dataverse_fetchxml_query","microsoft_dataverse_get_entity_metadata","microsoft_dataverse_get_record","microsoft_dataverse_list_records","microsoft_dataverse_search","microsoft_dataverse_update_multiple","microsoft_dataverse_update_record","microsoft_dataverse_upload_file","microsoft_dataverse_upsert_record","microsoft_dataverse_whoami","microsoft_dynamics_365_close_case","microsoft_dynamics_365_close_opportunity","microsoft_dynamics_365_create_record","microsoft_dynamics_365_get_record","microsoft_dynamics_365_list_records","microsoft_dynamics_365_qualify_lead","microsoft_dynamics_365_search_records","microsoft_dynamics_365_update_record","microsoft_excel_clear_range","microsoft_excel_create_table","microsoft_excel_delete_worksheet","microsoft_excel_format_range","microsoft_excel_read","microsoft_excel_read_v2","microsoft_excel_sort_range","microsoft_excel_table_add","microsoft_excel_worksheet_add","microsoft_excel_write","microsoft_excel_write_v2","microsoft_planner_create_bucket","microsoft_planner_create_plan","microsoft_planner_create_task","microsoft_planner_delete_bucket","microsoft_planner_delete_plan","microsoft_planner_delete_task","microsoft_planner_get_plan_details","microsoft_planner_get_task_details","microsoft_planner_list_buckets","microsoft_planner_list_plans","microsoft_planner_read_bucket","microsoft_planner_read_plan","microsoft_planner_read_task","microsoft_planner_update_bucket","microsoft_planner_update_plan","microsoft_planner_update_plan_details","microsoft_planner_update_task","microsoft_planner_update_task_details","microsoft_teams_delete_channel_message","microsoft_teams_delete_chat_message","microsoft_teams_get_message","microsoft_teams_list_channel_members","microsoft_teams_list_channels","microsoft_teams_list_chat_members","microsoft_teams_list_chats","microsoft_teams_list_team_members","microsoft_teams_list_teams","microsoft_teams_read_channel","microsoft_teams_read_chat","microsoft_teams_reply_to_message","microsoft_teams_set_reaction","microsoft_teams_unset_reaction","microsoft_teams_update_channel_message","microsoft_teams_update_chat_message","microsoft_teams_write_channel","microsoft_teams_write_chat","microsoft_word_append","microsoft_word_create","microsoft_word_create_from_template","microsoft_word_export_pdf","microsoft_word_list","microsoft_word_read","microsoft_word_replace_text","microsoft_word_update","millionverifier_get_credits","millionverifier_verify_email","mintlify_create_agent_job","mintlify_create_assistant_message","mintlify_detect_ai_prose","mintlify_get_agent_job","mintlify_get_assistant_caller_stats","mintlify_get_assistant_conversations","mintlify_get_feedback","mintlify_get_feedback_by_page","mintlify_get_page_content","mintlify_get_searches","mintlify_get_update_status","mintlify_get_views","mintlify_get_visitors","mintlify_search","mintlify_send_agent_message","mintlify_trigger_automation","mintlify_trigger_preview","mintlify_trigger_update","mistral_parser","mistral_parser_v2","mistral_parser_v3","modal_call_function","modal_chat_completion","modal_list_models","monday_archive_item","monday_change_column_value","monday_create_board","monday_create_column","monday_create_group","monday_create_item","monday_create_subitem","monday_create_update","monday_delete_item","monday_duplicate_item","monday_get_board","monday_get_groups","monday_get_item","monday_get_items","monday_list_boards","monday_move_item_to_group","monday_search_items","monday_update_item","mongodb_delete","mongodb_execute","mongodb_insert","mongodb_introspect","mongodb_query","mongodb_update","mssql_delete","mssql_execute","mssql_insert","mssql_introspect","mssql_query","mssql_update","mysql_delete","mysql_execute","mysql_insert","mysql_introspect","mysql_query","mysql_update","neo4j_create","neo4j_delete","neo4j_execute","neo4j_introspect","neo4j_merge","neo4j_query","neo4j_update","netsuite_attach_record","netsuite_batch_create_records","netsuite_batch_delete_records","netsuite_batch_get_records","netsuite_batch_update_records","netsuite_batch_upsert_records","netsuite_create_record","netsuite_delete_record","netsuite_detach_record","netsuite_execute_action","netsuite_execute_dataset","netsuite_execute_suiteql","netsuite_get_async_result","netsuite_get_async_status","netsuite_get_governance_limits","netsuite_get_record","netsuite_get_record_form","netsuite_get_record_metadata","netsuite_get_select_options","netsuite_get_server_time","netsuite_get_subresource","netsuite_list_datasets","netsuite_list_record_types","netsuite_list_records","netsuite_transform_record","netsuite_update_record","netsuite_upsert_record","neverbounce_get_credits","neverbounce_verify_email","new_relic_create_deployment_event","new_relic_get_entity","new_relic_nrql_query","new_relic_search_entities","notion_add_database_row","notion_add_database_row_v2","notion_append_blocks","notion_append_blocks_v2","notion_create_comment","notion_create_comment_v2","notion_create_database","notion_create_database_v2","notion_create_page","notion_create_page_v2","notion_delete_block","notion_delete_block_v2","notion_list_comments","notion_list_comments_v2","notion_list_users","notion_list_users_v2","notion_query_database","notion_query_database_v2","notion_read","notion_read_database","notion_read_database_v2","notion_read_v2","notion_retrieve_block","notion_retrieve_block_children","notion_retrieve_block_children_v2","notion_retrieve_block_v2","notion_retrieve_user","notion_retrieve_user_v2","notion_search","notion_search_v2","notion_update_block","notion_update_block_v2","notion_update_page","notion_update_page_v2","notion_write","notion_write_v2","obsidian_append_active","obsidian_append_note","obsidian_append_periodic_note","obsidian_create_note","obsidian_delete_note","obsidian_execute_command","obsidian_get_active","obsidian_get_note","obsidian_get_periodic_note","obsidian_list_commands","obsidian_list_files","obsidian_open_file","obsidian_patch_active","obsidian_patch_note","obsidian_search","okta_activate_group_rule","okta_activate_user","okta_add_user_to_group","okta_assign_group_to_app","okta_assign_user_role","okta_assign_user_to_app","okta_clear_user_sessions","okta_create_group","okta_create_group_rule","okta_create_user","okta_deactivate_group_rule","okta_deactivate_user","okta_delete_group","okta_delete_group_rule","okta_delete_user","okta_enroll_factor","okta_get_app","okta_get_factor","okta_get_group","okta_get_group_rule","okta_get_logs","okta_get_session","okta_get_user","okta_list_app_groups","okta_list_app_users","okta_list_apps","okta_list_factors","okta_list_group_members","okta_list_group_rules","okta_list_groups","okta_list_user_roles","okta_list_users","okta_remove_group_from_app","okta_remove_user_from_app","okta_remove_user_from_group","okta_remove_user_role","okta_reset_all_factors","okta_reset_factor","okta_reset_password","okta_revoke_session","okta_suspend_user","okta_unsuspend_user","okta_update_group","okta_update_user","onedrive_copy","onedrive_create_folder","onedrive_create_share_link","onedrive_delete","onedrive_download","onedrive_get_drive_info","onedrive_get_item","onedrive_list","onedrive_move","onedrive_search","onedrive_upload","onepassword_create_item","onepassword_delete_item","onepassword_get_item","onepassword_get_item_file","onepassword_get_vault","onepassword_list_items","onepassword_list_vaults","onepassword_replace_item","onepassword_resolve_secret","onepassword_update_item","openai_embeddings","openai_image","oracle_epm_profitability_apply_data_grants","oracle_epm_profitability_clear_pov","oracle_epm_profitability_copy_pov","oracle_epm_profitability_create_application","oracle_epm_profitability_deploy_cube","oracle_epm_profitability_download_file","oracle_epm_profitability_enable_application","oracle_epm_profitability_export_query_results","oracle_epm_profitability_generate_program_documentation","oracle_epm_profitability_get_rule_balancing","oracle_epm_profitability_get_task_status","oracle_epm_profitability_import_template","oracle_epm_profitability_list_files","oracle_epm_profitability_load_data","oracle_epm_profitability_merge_slices","oracle_epm_profitability_optimize_cube","oracle_epm_profitability_run_calculation","oracle_epm_profitability_update_dimensions","oracle_epm_profitability_upload_file","oracle_epm_profitability_wait_for_task","outlook_calendar_create_event","outlook_calendar_delete_event","outlook_calendar_get_event","outlook_calendar_list_events","outlook_calendar_respond","outlook_calendar_update_event","outlook_copy","outlook_create_folder","outlook_delete","outlook_draft","outlook_forward","outlook_get_attachment","outlook_list_attachments","outlook_list_folders","outlook_mark_read","outlook_mark_unread","outlook_move","outlook_read","outlook_reply","outlook_reply_all","outlook_search","outlook_send","outlook_update_message","pagerduty_add_note","pagerduty_create_incident","pagerduty_get_incident","pagerduty_get_service","pagerduty_list_escalation_policies","pagerduty_list_incident_alerts","pagerduty_list_incidents","pagerduty_list_oncalls","pagerduty_list_schedules","pagerduty_list_services","pagerduty_list_users","pagerduty_merge_incidents","pagerduty_send_event","pagerduty_snooze_incident","pagerduty_update_incident","parallel_deep_research","parallel_extract","parallel_search","pdl_autocomplete","pdl_bulk_company_enrich","pdl_bulk_person_enrich","pdl_clean_company","pdl_clean_location","pdl_clean_school","pdl_company_enrich","pdl_company_search","pdl_person_enrich","pdl_person_identify","pdl_person_search","perplexity_chat","perplexity_search","persona_approve_inquiry","persona_create_account","persona_create_inquiry","persona_create_report","persona_decline_inquiry","persona_expire_inquiry","persona_generate_inquiry_link","persona_get_account","persona_get_case","persona_get_document","persona_get_inquiry","persona_get_report","persona_get_verification","persona_import_accounts","persona_list_accounts","persona_list_cases","persona_list_inquiries","persona_list_inquiry_templates","persona_list_reports","persona_mark_inquiry_for_review","persona_print_inquiry_pdf","persona_redact_account","persona_redact_inquiry","persona_resume_inquiry","persona_update_account","persona_update_inquiry","pinecone_delete_vectors","pinecone_describe_index","pinecone_describe_index_stats","pinecone_fetch","pinecone_generate_embeddings","pinecone_list_indexes","pinecone_list_vector_ids","pinecone_search_text","pinecone_search_vector","pinecone_update_vector","pinecone_upsert_text","pipedrive_create_activity","pipedrive_create_deal","pipedrive_create_lead","pipedrive_create_project","pipedrive_delete_lead","pipedrive_get_activities","pipedrive_get_all_deals","pipedrive_get_deal","pipedrive_get_files","pipedrive_get_leads","pipedrive_get_mail_messages","pipedrive_get_mail_thread","pipedrive_get_pipeline_deals","pipedrive_get_pipelines","pipedrive_get_projects","pipedrive_update_activity","pipedrive_update_deal","pipedrive_update_lead","pitchbook_company_active_investors","pitchbook_company_bio","pitchbook_company_deal_service_providers","pitchbook_company_deals","pitchbook_company_financials","pitchbook_company_general_service_providers","pitchbook_company_industries","pitchbook_company_investors","pitchbook_company_most_recent_debt_financing","pitchbook_company_most_recent_financials","pitchbook_company_most_recent_financing","pitchbook_company_search","pitchbook_company_similar_companies","pitchbook_company_social_analytics","pitchbook_company_updates","pitchbook_company_vc_exit_predictions","pitchbook_contracts_history","pitchbook_cost_of_calls","pitchbook_credit_history","pitchbook_credit_news","pitchbook_credit_news_bulk","pitchbook_credit_news_most_recent","pitchbook_credit_news_search","pitchbook_deal_bio","pitchbook_deal_cap_table_history","pitchbook_deal_debt_lenders","pitchbook_deal_detailed","pitchbook_deal_investors","pitchbook_deal_multiples","pitchbook_deal_search","pitchbook_deal_service_providers","pitchbook_deal_stock_info","pitchbook_deal_tranche_info","pitchbook_deal_updates","pitchbook_deal_valuation","pitchbook_entity_affiliates","pitchbook_entity_locations","pitchbook_entity_news","pitchbook_entity_people","pitchbook_entity_updates","pitchbook_fund_active_investments","pitchbook_fund_benchmark","pitchbook_fund_bio","pitchbook_fund_cash_flows","pitchbook_fund_commitments","pitchbook_fund_investment_preferences","pitchbook_fund_investments","pitchbook_fund_performance","pitchbook_fund_search","pitchbook_fund_team","pitchbook_fund_updates","pitchbook_investor_active_investments","pitchbook_investor_bio","pitchbook_investor_board_seats","pitchbook_investor_deal_service_providers","pitchbook_investor_funds","pitchbook_investor_general_service_providers","pitchbook_investor_investments","pitchbook_investor_last_closed_fund","pitchbook_investor_preferences","pitchbook_investor_search","pitchbook_investor_updates","pitchbook_limited_partner_actual_allocations","pitchbook_limited_partner_bio","pitchbook_limited_partner_commitment_aggregates","pitchbook_limited_partner_commitment_preferences","pitchbook_limited_partner_commitments_detailed","pitchbook_limited_partner_search","pitchbook_limited_partner_service_providers","pitchbook_limited_partner_target_allocations","pitchbook_limited_partner_updates","pitchbook_lookup_table_structure","pitchbook_lookup_tables","pitchbook_patent_detailed","pitchbook_patent_search","pitchbook_people_search","pitchbook_person_bio","pitchbook_person_contact","pitchbook_person_education_work","pitchbook_sandbox_entities","pitchbook_search","pitchbook_service_provider_bio","pitchbook_service_provider_search","pitchbook_service_provider_updates","pitchbook_serviced_companies","pitchbook_serviced_deals","pitchbook_serviced_funds","pitchbook_serviced_investors","pitchbook_serviced_limited_partners","pitchbook_shared_search","pitchbook_usage_report","polymarket_get_activity","polymarket_get_event","polymarket_get_events","polymarket_get_holders","polymarket_get_last_trade_price","polymarket_get_leaderboard","polymarket_get_market","polymarket_get_markets","polymarket_get_midpoint","polymarket_get_orderbook","polymarket_get_positions","polymarket_get_price","polymarket_get_price_history","polymarket_get_series","polymarket_get_series_by_id","polymarket_get_spread","polymarket_get_tags","polymarket_get_tick_size","polymarket_get_trades","polymarket_search","postgresql_delete","postgresql_execute","postgresql_insert","postgresql_introspect","postgresql_query","postgresql_update","posthog_batch_events","posthog_capture_event","posthog_create_annotation","posthog_create_cohort","posthog_create_dashboard","posthog_create_experiment","posthog_create_feature_flag","posthog_create_insight","posthog_create_survey","posthog_delete_feature_flag","posthog_delete_person","posthog_delete_survey","posthog_evaluate_flags","posthog_get_cohort","posthog_get_dashboard","posthog_get_event_definition","posthog_get_experiment","posthog_get_feature_flag","posthog_get_insight","posthog_get_organization","posthog_get_person","posthog_get_project","posthog_get_property_definition","posthog_get_session_recording","posthog_get_survey","posthog_list_actions","posthog_list_annotations","posthog_list_cohorts","posthog_list_dashboards","posthog_list_event_definitions","posthog_list_experiments","posthog_list_feature_flags","posthog_list_insights","posthog_list_organizations","posthog_list_persons","posthog_list_projects","posthog_list_property_definitions","posthog_list_recording_playlists","posthog_list_session_recordings","posthog_list_surveys","posthog_query","posthog_update_cohort","posthog_update_event_definition","posthog_update_experiment","posthog_update_feature_flag","posthog_update_insight","posthog_update_property_definition","posthog_update_survey","profound_bot_logs","profound_bots_report","profound_category_assets","profound_category_personas","profound_category_prompts","profound_category_tags","profound_category_topics","profound_citation_prompts","profound_citations_report","profound_list_assets","profound_list_categories","profound_list_domains","profound_list_models","profound_list_optimizations","profound_list_personas","profound_list_regions","profound_optimization_analysis","profound_prompt_answers","profound_prompt_volume","profound_query_fanouts","profound_raw_logs","profound_referrals_report","profound_sentiment_report","profound_visibility_report","prospeo_account_information","prospeo_bulk_enrich_company","prospeo_bulk_enrich_person","prospeo_enrich_company","prospeo_enrich_person","prospeo_search_company","prospeo_search_person","prospeo_search_suggestions","pulse_parser","pulse_parser_v2","qdrant_fetch_points","qdrant_search_vector","qdrant_upsert_points","quartr_get_audio","quartr_get_company","quartr_get_event","quartr_get_event_summary","quartr_get_report","quartr_get_slide_deck","quartr_get_transcript","quartr_list_audio","quartr_list_companies","quartr_list_document_types","quartr_list_documents","quartr_list_event_types","quartr_list_events","quartr_list_live_events","quartr_list_reports","quartr_list_slide_decks","quartr_list_transcripts","quiver_image_to_svg","quiver_list_models","quiver_text_to_svg","rabbitmq_create_binding","rabbitmq_create_exchange","rabbitmq_create_policy","rabbitmq_create_queue","rabbitmq_delete_binding","rabbitmq_delete_exchange","rabbitmq_delete_policy","rabbitmq_delete_queue","rabbitmq_get_exchange","rabbitmq_get_messages","rabbitmq_get_overview","rabbitmq_get_queue","rabbitmq_health_check","rabbitmq_list_bindings","rabbitmq_list_channels","rabbitmq_list_connections","rabbitmq_list_consumers","rabbitmq_list_exchange_bindings","rabbitmq_list_exchanges","rabbitmq_list_nodes","rabbitmq_list_policies","rabbitmq_list_queues","rabbitmq_list_vhosts","rabbitmq_publish_message","rabbitmq_purge_queue","railway_create_environment","railway_create_project","railway_create_service","railway_delete_environment","railway_delete_project","railway_delete_service","railway_delete_variable","railway_deploy_service","railway_get_deployment","railway_get_deployment_logs","railway_get_project","railway_list_deployments","railway_list_project_members","railway_list_projects","railway_list_variables","railway_restart_deployment","railway_rollback_deployment","railway_transfer_project","railway_update_project","railway_upsert_variable","rb2b_credit_check","rb2b_email_to_activity","rb2b_hem_to_best_linkedin","rb2b_hem_to_business_profile","rb2b_hem_to_linkedin","rb2b_hem_to_maid","rb2b_ip_to_company","rb2b_ip_to_hem","rb2b_ip_to_maid","rb2b_linkedin_slug_search","rb2b_linkedin_to_best_personal_email","rb2b_linkedin_to_business_profile","rb2b_linkedin_to_hashed_emails","rb2b_linkedin_to_mobile_phone","rb2b_linkedin_to_personal_email","rds_delete","rds_execute","rds_insert","rds_introspect","rds_query","rds_update","reddit_delete","reddit_edit","reddit_get_comments","reddit_get_controversial","reddit_get_info","reddit_get_me","reddit_get_messages","reddit_get_posts","reddit_get_saved","reddit_get_subreddit_info","reddit_get_subreddit_rules","reddit_get_user","reddit_get_user_comments","reddit_get_user_posts","reddit_hide","reddit_hot_posts","reddit_list_my_subreddits","reddit_lock","reddit_mark_all_read","reddit_mark_read","reddit_marknsfw","reddit_mod_approve","reddit_mod_distinguish","reddit_mod_remove","reddit_mod_sticky","reddit_reply","reddit_report","reddit_save","reddit_search","reddit_search_subreddits","reddit_send_message","reddit_submit_post","reddit_subscribe","reddit_unhide","reddit_unlock","reddit_unmarknsfw","reddit_unsave","reddit_vote","redis_command","redis_delete","redis_exists","redis_expire","redis_get","redis_hdel","redis_hget","redis_hgetall","redis_hset","redis_incr","redis_incrby","redis_keys","redis_llen","redis_lpop","redis_lpush","redis_lrange","redis_persist","redis_rpop","redis_rpush","redis_set","redis_setnx","redis_ttl","reducto_parser","reducto_parser_v2","resend_cancel_email","resend_create_audience","resend_create_broadcast","resend_create_contact","resend_delete_audience","resend_delete_contact","resend_get_audience","resend_get_broadcast","resend_get_contact","resend_get_email","resend_list_audiences","resend_list_contacts","resend_list_domains","resend_send","resend_send_broadcast","resend_update_contact","revenuecat_create_purchase","revenuecat_defer_google_subscription","revenuecat_delete_customer","revenuecat_get_customer","revenuecat_grant_entitlement","revenuecat_list_offerings","revenuecat_refund_google_subscription","revenuecat_revoke_entitlement","revenuecat_revoke_google_subscription","revenuecat_update_subscriber_attributes","rippling_bulk_create_custom_object_records","rippling_bulk_delete_custom_object_records","rippling_bulk_update_custom_object_records","rippling_create_business_partner","rippling_create_business_partner_group","rippling_create_custom_app","rippling_create_custom_object","rippling_create_custom_object_field","rippling_create_custom_object_record","rippling_create_custom_page","rippling_create_custom_setting","rippling_create_department","rippling_create_draft_hires","rippling_create_object_category","rippling_create_title","rippling_create_work_location","rippling_delete_business_partner","rippling_delete_business_partner_group","rippling_delete_custom_app","rippling_delete_custom_object","rippling_delete_custom_object_field","rippling_delete_custom_object_record","rippling_delete_custom_page","rippling_delete_custom_setting","rippling_delete_object_category","rippling_delete_title","rippling_delete_work_location","rippling_get_business_partner","rippling_get_business_partner_group","rippling_get_current_user","rippling_get_custom_app","rippling_get_custom_object","rippling_get_custom_object_field","rippling_get_custom_object_record","rippling_get_custom_object_record_by_external_id","rippling_get_custom_page","rippling_get_custom_setting","rippling_get_department","rippling_get_employment_type","rippling_get_job_function","rippling_get_object_category","rippling_get_report_run","rippling_get_supergroup","rippling_get_team","rippling_get_title","rippling_get_user","rippling_get_work_location","rippling_get_worker","rippling_list_business_partner_groups","rippling_list_business_partners","rippling_list_companies","rippling_list_custom_apps","rippling_list_custom_fields","rippling_list_custom_object_fields","rippling_list_custom_object_records","rippling_list_custom_objects","rippling_list_custom_pages","rippling_list_custom_settings","rippling_list_departments","rippling_list_employment_types","rippling_list_entitlements","rippling_list_job_functions","rippling_list_object_categories","rippling_list_supergroup_exclusion_members","rippling_list_supergroup_inclusion_members","rippling_list_supergroup_members","rippling_list_supergroups","rippling_list_teams","rippling_list_titles","rippling_list_users","rippling_list_work_locations","rippling_list_workers","rippling_query_custom_object_records","rippling_trigger_report_run","rippling_update_custom_app","rippling_update_custom_object","rippling_update_custom_object_field","rippling_update_custom_object_record","rippling_update_custom_page","rippling_update_custom_setting","rippling_update_department","rippling_update_object_category","rippling_update_supergroup_exclusion_members","rippling_update_supergroup_inclusion_members","rippling_update_title","rippling_update_work_location","rocketlane_add_field_option","rocketlane_add_project_members","rocketlane_add_task_assignees","rocketlane_add_task_dependencies","rocketlane_add_task_followers","rocketlane_archive_project","rocketlane_assign_placeholders","rocketlane_create_field","rocketlane_create_phase","rocketlane_create_project","rocketlane_create_space","rocketlane_create_space_document","rocketlane_create_task","rocketlane_create_time_entry","rocketlane_create_time_off","rocketlane_delete_field","rocketlane_delete_phase","rocketlane_delete_project","rocketlane_delete_space","rocketlane_delete_space_document","rocketlane_delete_task","rocketlane_delete_time_entry","rocketlane_delete_time_off","rocketlane_get_field","rocketlane_get_invoice","rocketlane_get_invoice_line_items","rocketlane_get_invoice_payments","rocketlane_get_phase","rocketlane_get_project","rocketlane_get_space","rocketlane_get_space_document","rocketlane_get_task","rocketlane_get_time_entry","rocketlane_get_time_off","rocketlane_get_user","rocketlane_import_template","rocketlane_list_fields","rocketlane_list_invoices","rocketlane_list_phases","rocketlane_list_placeholders","rocketlane_list_projects","rocketlane_list_resource_allocations","rocketlane_list_space_documents","rocketlane_list_spaces","rocketlane_list_tasks","rocketlane_list_time_entries","rocketlane_list_time_entry_categories","rocketlane_list_time_offs","rocketlane_list_users","rocketlane_move_task_to_phase","rocketlane_remove_project_members","rocketlane_remove_task_assignees","rocketlane_remove_task_dependencies","rocketlane_remove_task_followers","rocketlane_search_time_entries","rocketlane_unassign_placeholders","rocketlane_update_field","rocketlane_update_field_option","rocketlane_update_phase","rocketlane_update_project","rocketlane_update_space","rocketlane_update_space_document","rocketlane_update_task","rocketlane_update_time_entry","rootly_acknowledge_alert","rootly_add_incident_event","rootly_add_subscribers","rootly_assign_incident_role","rootly_create_action_item","rootly_create_alert","rootly_create_incident","rootly_create_status_page_event","rootly_delete_action_item","rootly_delete_incident","rootly_escalate_alert","rootly_get_alert","rootly_get_incident","rootly_list_action_items","rootly_list_alerts","rootly_list_causes","rootly_list_environments","rootly_list_escalation_policies","rootly_list_functionalities","rootly_list_incident_events","rootly_list_incident_roles","rootly_list_incident_types","rootly_list_incidents","rootly_list_on_calls","rootly_list_playbooks","rootly_list_retrospectives","rootly_list_schedules","rootly_list_services","rootly_list_severities","rootly_list_teams","rootly_list_users","rootly_mitigate_incident","rootly_remove_subscribers","rootly_resolve_alert","rootly_resolve_incident","rootly_run_workflow","rootly_snooze_alert","rootly_unassign_incident_role","rootly_update_action_item","rootly_update_alert","rootly_update_incident","s3_copy_object","s3_create_bucket","s3_delete_bucket","s3_delete_object","s3_delete_objects","s3_get_object","s3_head_object","s3_list_buckets","s3_list_objects","s3_presigned_url","s3_put_object","sailpoint_approve_access_request","sailpoint_cancel_access_request","sailpoint_decide_certification_review_items","sailpoint_get_access_profile","sailpoint_get_access_profile_entitlements","sailpoint_get_access_request_config","sailpoint_get_access_request_status","sailpoint_get_account","sailpoint_get_account_activity","sailpoint_get_account_entitlements","sailpoint_get_account_selections","sailpoint_get_campaign","sailpoint_get_certification","sailpoint_get_entitlement","sailpoint_get_entitlement_request_config","sailpoint_get_identity","sailpoint_get_role","sailpoint_get_role_entitlements","sailpoint_get_source","sailpoint_get_task_status","sailpoint_list_access_profiles","sailpoint_list_account_activities","sailpoint_list_accounts","sailpoint_list_campaigns","sailpoint_list_certification_review_items","sailpoint_list_certifications","sailpoint_list_entitlements","sailpoint_list_identities","sailpoint_list_identity_entitlements","sailpoint_list_pending_access_request_approvals","sailpoint_list_roles","sailpoint_list_sources","sailpoint_load_accounts","sailpoint_load_entitlements","sailpoint_reject_access_request","sailpoint_request_access","sailpoint_search","sailpoint_search_aggregate","sailpoint_search_count","sailpoint_sign_off_certification","salesforce_create_account","salesforce_create_case","salesforce_create_contact","salesforce_create_custom_field","salesforce_create_custom_object","salesforce_create_lead","salesforce_create_opportunity","salesforce_create_task","salesforce_delete_account","salesforce_delete_case","salesforce_delete_contact","salesforce_delete_custom_field","salesforce_delete_lead","salesforce_delete_opportunity","salesforce_delete_task","salesforce_describe_object","salesforce_get_accounts","salesforce_get_cases","salesforce_get_contacts","salesforce_get_dashboard","salesforce_get_leads","salesforce_get_opportunities","salesforce_get_report","salesforce_get_tasks","salesforce_list_dashboards","salesforce_list_objects","salesforce_list_report_types","salesforce_list_reports","salesforce_query","salesforce_query_more","salesforce_refresh_dashboard","salesforce_run_report","salesforce_tooling_query","salesforce_update_account","salesforce_update_case","salesforce_update_contact","salesforce_update_custom_field","salesforce_update_lead","salesforce_update_opportunity","salesforce_update_task","sap_concur_approve_expense_report","sap_concur_associate_attendees","sap_concur_create_cash_advance","sap_concur_create_expected_expense","sap_concur_create_expense_report","sap_concur_create_list_item","sap_concur_create_purchase_request","sap_concur_create_quick_expense","sap_concur_create_quick_expense_with_image","sap_concur_create_report_comment","sap_concur_create_travel_request","sap_concur_create_user","sap_concur_delete_expected_expense","sap_concur_delete_expense","sap_concur_delete_expense_report","sap_concur_delete_list_item","sap_concur_delete_travel_request","sap_concur_delete_user","sap_concur_get_allocation","sap_concur_get_budget","sap_concur_get_cash_advance","sap_concur_get_expected_expense","sap_concur_get_expense","sap_concur_get_expense_report","sap_concur_get_itemizations","sap_concur_get_itinerary","sap_concur_get_list","sap_concur_get_list_item","sap_concur_get_purchase_request","sap_concur_get_receipt","sap_concur_get_receipt_status","sap_concur_get_request_cash_advance","sap_concur_get_travel_profile","sap_concur_get_travel_request","sap_concur_get_user","sap_concur_issue_cash_advance","sap_concur_list_allocations","sap_concur_list_attendee_associations","sap_concur_list_budget_categories","sap_concur_list_budgets","sap_concur_list_exceptions","sap_concur_list_expected_expenses","sap_concur_list_expense_reports","sap_concur_list_expenses","sap_concur_list_itineraries","sap_concur_list_list_items","sap_concur_list_lists","sap_concur_list_receipts","sap_concur_list_report_comments","sap_concur_list_reports_to_approve","sap_concur_list_travel_profiles_summary","sap_concur_list_travel_request_comments","sap_concur_list_travel_requests","sap_concur_list_users","sap_concur_move_travel_request","sap_concur_recall_expense_report","sap_concur_remove_all_attendees","sap_concur_search_locations","sap_concur_search_users","sap_concur_send_back_expense_report","sap_concur_submit_expense_report","sap_concur_update_allocation","sap_concur_update_expected_expense","sap_concur_update_expense","sap_concur_update_expense_report","sap_concur_update_list_item","sap_concur_update_travel_request","sap_concur_update_user","sap_concur_upload_exchange_rates","sap_concur_upload_receipt_image","sap_s4hana_create_business_partner","sap_s4hana_create_purchase_order","sap_s4hana_create_purchase_requisition","sap_s4hana_create_sales_order","sap_s4hana_delete_sales_order","sap_s4hana_get_billing_document","sap_s4hana_get_business_partner","sap_s4hana_get_customer","sap_s4hana_get_inbound_delivery","sap_s4hana_get_material_document","sap_s4hana_get_outbound_delivery","sap_s4hana_get_product","sap_s4hana_get_purchase_order","sap_s4hana_get_purchase_requisition","sap_s4hana_get_sales_order","sap_s4hana_get_supplier","sap_s4hana_get_supplier_invoice","sap_s4hana_list_billing_documents","sap_s4hana_list_business_partners","sap_s4hana_list_customers","sap_s4hana_list_inbound_deliveries","sap_s4hana_list_material_documents","sap_s4hana_list_material_stock","sap_s4hana_list_outbound_deliveries","sap_s4hana_list_products","sap_s4hana_list_purchase_orders","sap_s4hana_list_purchase_requisitions","sap_s4hana_list_sales_orders","sap_s4hana_list_supplier_invoices","sap_s4hana_list_suppliers","sap_s4hana_odata_query","sap_s4hana_update_business_partner","sap_s4hana_update_customer","sap_s4hana_update_product","sap_s4hana_update_purchase_order","sap_s4hana_update_purchase_requisition","sap_s4hana_update_sales_order","sap_s4hana_update_supplier","search_tool","secrets_manager_create_secret","secrets_manager_delete_secret","secrets_manager_describe_secret","secrets_manager_get_secret","secrets_manager_list_secrets","secrets_manager_restore_secret","secrets_manager_rotate_secret","secrets_manager_tag_resource","secrets_manager_untag_resource","secrets_manager_update_secret","semrush_backlinks","semrush_backlinks_anchors","semrush_backlinks_competitors","semrush_backlinks_geo_distribution","semrush_backlinks_indexed_pages","semrush_backlinks_overview","semrush_backlinks_tld_distribution","semrush_batch_keyword_overview","semrush_broad_match_keywords","semrush_domain_ad_copies","semrush_domain_ad_history","semrush_domain_organic_competitors","semrush_domain_organic_keywords","semrush_domain_overview","semrush_domain_overview_all","semrush_domain_overview_history","semrush_domain_paid_competitors","semrush_domain_paid_keywords","semrush_domain_pla_copies","semrush_domain_pla_keywords","semrush_domain_vs_domain","semrush_keyword_ad_history","semrush_keyword_difficulty","semrush_keyword_overview","semrush_keyword_overview_all","semrush_keyword_questions","semrush_organic_results","semrush_paid_results","semrush_referring_domains","semrush_referring_ips","semrush_related_keywords","semrush_subdomain_ad_copies","semrush_subdomain_organic_keywords","semrush_subdomain_overview","semrush_subdomain_overview_all","semrush_subdomain_overview_history","semrush_subdomain_paid_keywords","semrush_top_domains","semrush_url_organic_keywords","semrush_url_overview","semrush_url_overview_all","semrush_url_overview_history","semrush_url_paid_keywords","semrush_winners_and_losers","sendblue_evaluate_service","sendblue_get_message","sendblue_send_group_message","sendblue_send_message","sendblue_send_typing_indicator","sendgrid_add_contact","sendgrid_add_contacts_to_list","sendgrid_create_list","sendgrid_create_template","sendgrid_create_template_version","sendgrid_delete_contacts","sendgrid_delete_list","sendgrid_delete_template","sendgrid_get_contact","sendgrid_get_list","sendgrid_get_template","sendgrid_list_all_lists","sendgrid_list_templates","sendgrid_remove_contacts_from_list","sendgrid_search_contacts","sendgrid_send_mail","sentry_events_get","sentry_events_list","sentry_issues_get","sentry_issues_list","sentry_issues_update","sentry_projects_create","sentry_projects_get","sentry_projects_list","sentry_projects_update","sentry_releases_create","sentry_releases_deploy","sentry_releases_list","sentry_teams_list","serper_search","servicenow_add_incident_comment","servicenow_aggregate","servicenow_close_incident","servicenow_create_change_request","servicenow_create_incident","servicenow_create_record","servicenow_delete_record","servicenow_download_attachment","servicenow_find_user","servicenow_get_change_next_states","servicenow_get_change_request","servicenow_get_ci","servicenow_get_incident","servicenow_get_knowledge_article","servicenow_get_requested_item","servicenow_list_approvals","servicenow_list_attachments","servicenow_list_catalog_items","servicenow_list_change_requests","servicenow_list_change_tasks","servicenow_list_ci_relationships","servicenow_list_group_members","servicenow_list_incidents","servicenow_list_requested_items","servicenow_order_catalog_item","servicenow_read_record","servicenow_resolve_incident","servicenow_search_cis","servicenow_search_knowledge","servicenow_update_approval","servicenow_update_change_request","servicenow_update_change_state","servicenow_update_incident","servicenow_update_record","servicenow_upload_attachment","ses_create_configuration_set","ses_create_email_identity","ses_create_template","ses_delete_email_identity","ses_delete_suppressed_destination","ses_delete_template","ses_get_account","ses_get_email_identity","ses_get_suppressed_destination","ses_get_template","ses_list_identities","ses_list_suppressed_destinations","ses_list_templates","ses_put_suppressed_destination","ses_send_bulk_email","ses_send_custom_verification_email","ses_send_email","ses_send_templated_email","ses_update_template","sftp_delete","sftp_download","sftp_list","sftp_mkdir","sftp_upload","sharepoint_add_list_items","sharepoint_create_list","sharepoint_create_page","sharepoint_delete_file","sharepoint_delete_list_item","sharepoint_delete_page","sharepoint_download_file","sharepoint_get_drive_item","sharepoint_get_list","sharepoint_get_list_item","sharepoint_list_sites","sharepoint_publish_page","sharepoint_read_page","sharepoint_update_list","sharepoint_update_page","sharepoint_upload_file","shopify_adjust_inventory","shopify_cancel_order","shopify_create_customer","shopify_create_fulfillment","shopify_create_product","shopify_delete_customer","shopify_delete_product","shopify_get_collection","shopify_get_customer","shopify_get_inventory_level","shopify_get_order","shopify_get_product","shopify_list_collections","shopify_list_customers","shopify_list_inventory_items","shopify_list_locations","shopify_list_orders","shopify_list_products","shopify_update_customer","shopify_update_order","shopify_update_product","similarweb_bounce_rate","similarweb_page_views","similarweb_pages_per_visit","similarweb_traffic_visits","similarweb_visit_duration","similarweb_website_overview","sixtyfour_enrich_company","sixtyfour_enrich_lead","sixtyfour_find_email","sixtyfour_find_phone","slack_add_reaction","slack_archive_conversation","slack_canvas","slack_create_channel_canvas","slack_create_conversation","slack_delete_canvas","slack_delete_message","slack_delete_scheduled_message","slack_download","slack_edit_canvas","slack_ephemeral_message","slack_get_canvas","slack_get_channel_history","slack_get_channel_info","slack_get_message","slack_get_permalink","slack_get_thread","slack_get_thread_replies","slack_get_user","slack_get_user_presence","slack_invite_to_conversation","slack_list_canvases","slack_list_channels","slack_list_members","slack_list_scheduled_messages","slack_list_users","slack_lookup_canvas_sections","slack_message","slack_message_reader","slack_open_view","slack_publish_view","slack_push_view","slack_remove_reaction","slack_rename_agent_session_v2","slack_rename_conversation","slack_schedule_message","slack_set_agent_session_status_v2","slack_set_conversation_purpose","slack_set_conversation_topic","slack_set_status","slack_set_suggested_prompts","slack_set_suggested_prompts_v2","slack_set_title","slack_update_message","slack_update_view","smartlead_add_email_accounts_to_campaign","smartlead_add_leads_to_campaign","smartlead_create_campaign","smartlead_create_lead_list","smartlead_delete_campaign","smartlead_delete_campaign_webhook","smartlead_delete_lead_from_campaign","smartlead_delete_lead_list","smartlead_duplicate_campaign","smartlead_export_campaign_leads","smartlead_get_campaign","smartlead_get_campaign_analytics","smartlead_get_campaign_analytics_by_date","smartlead_get_campaign_lead_statistics","smartlead_get_campaign_mailbox_statistics","smartlead_get_campaign_sequences","smartlead_get_campaign_statistics","smartlead_get_campaign_top_level_analytics_by_date","smartlead_get_campaign_webhook_summary","smartlead_get_lead_by_email","smartlead_get_lead_by_id","smartlead_get_lead_list","smartlead_get_lead_message_history","smartlead_list_campaign_email_accounts","smartlead_list_campaign_leads","smartlead_list_campaign_webhooks","smartlead_list_campaigns","smartlead_list_clients","smartlead_list_email_accounts","smartlead_list_inbox_replies","smartlead_list_lead_activities","smartlead_list_lead_categories","smartlead_list_lead_lists","smartlead_mark_lead_complete","smartlead_pause_lead","smartlead_remove_email_accounts_from_campaign","smartlead_resume_lead","smartlead_save_campaign_sequences","smartlead_unsubscribe_lead_from_campaign","smartlead_unsubscribe_lead_globally","smartlead_update_campaign_schedule","smartlead_update_campaign_settings","smartlead_update_campaign_status","smartlead_update_lead","smartlead_update_lead_category","smartlead_update_lead_list","smartlead_upsert_campaign_webhook","sms_send","smtp_send_mail","snowflake_alter_warehouse","snowflake_call_procedure","snowflake_cancel_statement","snowflake_cancel_task_run","snowflake_delete_rows","snowflake_execute_sql","snowflake_get_statement","snowflake_get_task","snowflake_get_task_run","snowflake_get_task_run_output","snowflake_get_warehouse","snowflake_insert_rows","snowflake_introspect_schema","snowflake_list_copy_history","snowflake_list_databases","snowflake_list_query_history","snowflake_list_schemas","snowflake_list_tables","snowflake_list_task_runs","snowflake_list_tasks","snowflake_list_warehouses","snowflake_load_data","snowflake_resume_task","snowflake_resume_warehouse","snowflake_run_task","snowflake_suspend_task","snowflake_suspend_warehouse","snowflake_unload_data","snowflake_update_rows","snowflake_upsert_rows","splunk_cancel_search_job","splunk_create_search_job","splunk_dispatch_saved_search","splunk_get_fired_alerts","splunk_get_saved_search","splunk_get_search_job","splunk_get_search_results","splunk_list_apps","splunk_list_fired_alerts","splunk_list_indexes","splunk_list_saved_searches","splunk_run_search","sportmonks_core_get_cities","sportmonks_core_get_city","sportmonks_core_get_continent","sportmonks_core_get_continents","sportmonks_core_get_countries","sportmonks_core_get_country","sportmonks_core_get_entity_filters","sportmonks_core_get_my_usage","sportmonks_core_get_region","sportmonks_core_get_regions","sportmonks_core_get_timezones","sportmonks_core_get_type","sportmonks_core_get_type_by_entity","sportmonks_core_get_types","sportmonks_core_search_cities","sportmonks_core_search_countries","sportmonks_core_search_regions","sportmonks_football_expected_by_player","sportmonks_football_expected_by_team","sportmonks_football_get_all_commentaries","sportmonks_football_get_all_fixtures","sportmonks_football_get_all_players","sportmonks_football_get_all_rivals","sportmonks_football_get_all_teams","sportmonks_football_get_all_transfer_rumours","sportmonks_football_get_all_transfers","sportmonks_football_get_brackets_by_season","sportmonks_football_get_coach","sportmonks_football_get_coaches","sportmonks_football_get_coaches_by_country","sportmonks_football_get_commentaries_by_fixture","sportmonks_football_get_current_leagues_by_team","sportmonks_football_get_expected_lineups_by_player","sportmonks_football_get_expected_lineups_by_team","sportmonks_football_get_extended_team_squad","sportmonks_football_get_fixture","sportmonks_football_get_fixtures_by_date","sportmonks_football_get_fixtures_by_date_range","sportmonks_football_get_fixtures_by_date_range_for_team","sportmonks_football_get_fixtures_by_ids","sportmonks_football_get_grouped_standings_by_round","sportmonks_football_get_head_to_head","sportmonks_football_get_inplay_livescores","sportmonks_football_get_latest_coaches","sportmonks_football_get_latest_fixtures","sportmonks_football_get_latest_livescores","sportmonks_football_get_latest_players","sportmonks_football_get_latest_totw","sportmonks_football_get_latest_transfers","sportmonks_football_get_league","sportmonks_football_get_leagues","sportmonks_football_get_leagues_by_country","sportmonks_football_get_leagues_by_date","sportmonks_football_get_leagues_by_team","sportmonks_football_get_live_leagues","sportmonks_football_get_live_probabilities","sportmonks_football_get_live_probabilities_by_fixture","sportmonks_football_get_live_standings_by_league","sportmonks_football_get_livescores","sportmonks_football_get_match_facts","sportmonks_football_get_match_facts_by_date_range","sportmonks_football_get_match_facts_by_fixture","sportmonks_football_get_match_facts_by_league","sportmonks_football_get_past_fixtures_by_tv_station","sportmonks_football_get_player","sportmonks_football_get_players_by_country","sportmonks_football_get_postmatch_news","sportmonks_football_get_postmatch_news_by_season","sportmonks_football_get_predictability_by_league","sportmonks_football_get_prematch_news","sportmonks_football_get_prematch_news_by_season","sportmonks_football_get_prematch_news_upcoming","sportmonks_football_get_probabilities","sportmonks_football_get_probabilities_by_fixture","sportmonks_football_get_referee","sportmonks_football_get_referees","sportmonks_football_get_referees_by_country","sportmonks_football_get_referees_by_season","sportmonks_football_get_rivals_by_team","sportmonks_football_get_round","sportmonks_football_get_round_statistics","sportmonks_football_get_rounds","sportmonks_football_get_rounds_by_season","sportmonks_football_get_schedules_by_season","sportmonks_football_get_schedules_by_season_and_team","sportmonks_football_get_schedules_by_team","sportmonks_football_get_season","sportmonks_football_get_seasons","sportmonks_football_get_seasons_by_team","sportmonks_football_get_stage","sportmonks_football_get_stage_statistics","sportmonks_football_get_stages","sportmonks_football_get_stages_by_season","sportmonks_football_get_standing_corrections_by_season","sportmonks_football_get_standings","sportmonks_football_get_standings_by_round","sportmonks_football_get_standings_by_season","sportmonks_football_get_state","sportmonks_football_get_states","sportmonks_football_get_team","sportmonks_football_get_team_rankings","sportmonks_football_get_team_rankings_by_date","sportmonks_football_get_team_rankings_by_team","sportmonks_football_get_team_squad","sportmonks_football_get_team_squad_by_season","sportmonks_football_get_teams_by_country","sportmonks_football_get_teams_by_season","sportmonks_football_get_topscorers_by_season","sportmonks_football_get_topscorers_by_stage","sportmonks_football_get_totw","sportmonks_football_get_totw_by_round","sportmonks_football_get_transfer","sportmonks_football_get_transfer_rumour","sportmonks_football_get_transfer_rumours_between_dates","sportmonks_football_get_transfer_rumours_by_player","sportmonks_football_get_transfer_rumours_by_team","sportmonks_football_get_transfers_between_dates","sportmonks_football_get_transfers_by_player","sportmonks_football_get_transfers_by_team","sportmonks_football_get_tv_station","sportmonks_football_get_tv_stations","sportmonks_football_get_tv_stations_by_fixture","sportmonks_football_get_upcoming_fixtures_by_market","sportmonks_football_get_upcoming_fixtures_by_tv_station","sportmonks_football_get_value_bets","sportmonks_football_get_value_bets_by_fixture","sportmonks_football_get_venue","sportmonks_football_get_venues","sportmonks_football_get_venues_by_season","sportmonks_football_search_coaches","sportmonks_football_search_fixtures","sportmonks_football_search_leagues","sportmonks_football_search_players","sportmonks_football_search_referees","sportmonks_football_search_rounds","sportmonks_football_search_seasons","sportmonks_football_search_stages","sportmonks_football_search_teams","sportmonks_football_search_venues","sportmonks_motorsport_get_all_fixtures","sportmonks_motorsport_get_current_leagues_by_team","sportmonks_motorsport_get_driver","sportmonks_motorsport_get_driver_standings","sportmonks_motorsport_get_driver_standings_by_season","sportmonks_motorsport_get_drivers","sportmonks_motorsport_get_drivers_by_country","sportmonks_motorsport_get_drivers_by_season","sportmonks_motorsport_get_fixture","sportmonks_motorsport_get_fixtures_by_date","sportmonks_motorsport_get_fixtures_by_date_range","sportmonks_motorsport_get_fixtures_by_ids","sportmonks_motorsport_get_laps_by_fixture","sportmonks_motorsport_get_laps_by_fixture_and_driver","sportmonks_motorsport_get_laps_by_fixture_and_lap","sportmonks_motorsport_get_latest_laps_by_fixture","sportmonks_motorsport_get_latest_pitstops_by_fixture","sportmonks_motorsport_get_latest_stints_by_fixture","sportmonks_motorsport_get_latest_updated_drivers","sportmonks_motorsport_get_latest_updated_fixtures","sportmonks_motorsport_get_league","sportmonks_motorsport_get_leagues","sportmonks_motorsport_get_leagues_by_country","sportmonks_motorsport_get_leagues_by_date","sportmonks_motorsport_get_leagues_by_live","sportmonks_motorsport_get_leagues_by_team","sportmonks_motorsport_get_livescores","sportmonks_motorsport_get_pitstops_by_fixture","sportmonks_motorsport_get_pitstops_by_fixture_and_driver","sportmonks_motorsport_get_pitstops_by_fixture_and_lap","sportmonks_motorsport_get_race_results_by_season_and_driver","sportmonks_motorsport_get_race_results_by_season_and_team","sportmonks_motorsport_get_schedules_by_season","sportmonks_motorsport_get_season","sportmonks_motorsport_get_seasons","sportmonks_motorsport_get_stage","sportmonks_motorsport_get_stages","sportmonks_motorsport_get_stages_by_season","sportmonks_motorsport_get_state","sportmonks_motorsport_get_states","sportmonks_motorsport_get_stints_by_fixture","sportmonks_motorsport_get_stints_by_fixture_and_driver","sportmonks_motorsport_get_stints_by_fixture_and_stint","sportmonks_motorsport_get_team","sportmonks_motorsport_get_team_standings","sportmonks_motorsport_get_team_standings_by_season","sportmonks_motorsport_get_teams","sportmonks_motorsport_get_teams_by_country","sportmonks_motorsport_get_teams_by_season","sportmonks_motorsport_get_venue","sportmonks_motorsport_get_venues","sportmonks_motorsport_get_venues_by_season","sportmonks_motorsport_search_drivers","sportmonks_motorsport_search_leagues","sportmonks_motorsport_search_stages","sportmonks_motorsport_search_teams","sportmonks_motorsport_search_venues","sportmonks_odds_get_all_historical_odds","sportmonks_odds_get_all_inplay_odds","sportmonks_odds_get_all_pre_match_odds","sportmonks_odds_get_all_premium_odds","sportmonks_odds_get_bookmaker","sportmonks_odds_get_bookmaker_event_ids_by_fixture","sportmonks_odds_get_bookmakers","sportmonks_odds_get_bookmakers_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker","sportmonks_odds_get_inplay_odds_by_fixture_and_market","sportmonks_odds_get_last_updated_inplay_odds","sportmonks_odds_get_last_updated_pre_match_odds","sportmonks_odds_get_market","sportmonks_odds_get_markets","sportmonks_odds_get_pre_match_odds_by_fixture","sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker","sportmonks_odds_get_pre_match_odds_by_fixture_and_market","sportmonks_odds_get_premium_odds_by_fixture","sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker","sportmonks_odds_get_premium_odds_by_fixture_and_market","sportmonks_odds_get_updated_historical_odds_between","sportmonks_odds_get_updated_premium_odds_between","sportmonks_odds_search_bookmakers","sportmonks_odds_search_markets","spotify_add_playlist_cover","spotify_add_to_queue","spotify_add_tracks_to_playlist","spotify_check_following","spotify_check_playlist_followers","spotify_check_saved_albums","spotify_check_saved_audiobooks","spotify_check_saved_episodes","spotify_check_saved_shows","spotify_check_saved_tracks","spotify_create_playlist","spotify_follow_artists","spotify_follow_playlist","spotify_get_album","spotify_get_album_tracks","spotify_get_albums","spotify_get_artist","spotify_get_artist_albums","spotify_get_artist_top_tracks","spotify_get_artists","spotify_get_audiobook","spotify_get_audiobook_chapters","spotify_get_audiobooks","spotify_get_categories","spotify_get_current_user","spotify_get_currently_playing","spotify_get_devices","spotify_get_episode","spotify_get_episodes","spotify_get_followed_artists","spotify_get_markets","spotify_get_new_releases","spotify_get_playback_state","spotify_get_playlist","spotify_get_playlist_cover","spotify_get_playlist_tracks","spotify_get_queue","spotify_get_recently_played","spotify_get_saved_albums","spotify_get_saved_audiobooks","spotify_get_saved_episodes","spotify_get_saved_shows","spotify_get_saved_tracks","spotify_get_show","spotify_get_show_episodes","spotify_get_shows","spotify_get_top_artists","spotify_get_top_tracks","spotify_get_track","spotify_get_tracks","spotify_get_user_playlists","spotify_get_user_profile","spotify_pause","spotify_play","spotify_remove_saved_albums","spotify_remove_saved_audiobooks","spotify_remove_saved_episodes","spotify_remove_saved_shows","spotify_remove_saved_tracks","spotify_remove_tracks_from_playlist","spotify_reorder_playlist_items","spotify_replace_playlist_items","spotify_save_albums","spotify_save_audiobooks","spotify_save_episodes","spotify_save_shows","spotify_save_tracks","spotify_search","spotify_seek","spotify_set_repeat","spotify_set_shuffle","spotify_set_volume","spotify_skip_next","spotify_skip_previous","spotify_transfer_playback","spotify_unfollow_artists","spotify_unfollow_playlist","spotify_update_playlist","sqs_send","square_batch_retrieve_inventory_counts","square_cancel_invoice","square_cancel_payment","square_complete_payment","square_create_catalog_image","square_create_customer","square_create_invoice","square_create_order","square_create_payment","square_delete_catalog_object","square_delete_customer","square_delete_invoice","square_get_catalog_object","square_get_customer","square_get_invoice","square_get_location","square_get_order","square_get_payment","square_get_refund","square_list_catalog","square_list_customers","square_list_invoices","square_list_locations","square_list_payments","square_list_refunds","square_pay_order","square_publish_invoice","square_refund_payment","square_search_catalog_objects","square_search_customers","square_search_invoices","square_search_orders","square_update_customer","square_upsert_catalog_object","ssh_check_command_exists","ssh_check_file_exists","ssh_create_directory","ssh_delete_file","ssh_download_file","ssh_execute_command","ssh_execute_script","ssh_get_system_info","ssh_list_directory","ssh_move_rename","ssh_read_file_content","ssh_upload_file","ssh_write_file_content","stagehand_agent","stagehand_extract","stripe_cancel_payment_intent","stripe_cancel_subscription","stripe_capture_charge","stripe_capture_payment_intent","stripe_confirm_payment_intent","stripe_create_charge","stripe_create_customer","stripe_create_invoice","stripe_create_payment_intent","stripe_create_price","stripe_create_product","stripe_create_subscription","stripe_delete_customer","stripe_delete_invoice","stripe_delete_product","stripe_finalize_invoice","stripe_list_charges","stripe_list_customers","stripe_list_events","stripe_list_invoices","stripe_list_payment_intents","stripe_list_prices","stripe_list_products","stripe_list_subscriptions","stripe_pay_invoice","stripe_resume_subscription","stripe_retrieve_charge","stripe_retrieve_customer","stripe_retrieve_event","stripe_retrieve_invoice","stripe_retrieve_payment_intent","stripe_retrieve_price","stripe_retrieve_product","stripe_retrieve_subscription","stripe_search_charges","stripe_search_customers","stripe_search_invoices","stripe_search_payment_intents","stripe_search_prices","stripe_search_products","stripe_search_subscriptions","stripe_send_invoice","stripe_update_charge","stripe_update_customer","stripe_update_invoice","stripe_update_payment_intent","stripe_update_price","stripe_update_product","stripe_update_subscription","stripe_void_invoice","sts_assume_role","sts_assume_role_with_saml","sts_assume_role_with_web_identity","sts_get_access_key_info","sts_get_caller_identity","sts_get_session_token","stt_assemblyai","stt_assemblyai_v2","stt_deepgram","stt_deepgram_v2","stt_elevenlabs","stt_elevenlabs_v2","stt_gemini","stt_gemini_v2","stt_whisper","stt_whisper_v2","supabase_count","supabase_delete","supabase_get_row","supabase_insert","supabase_introspect","supabase_invoke_function","supabase_query","supabase_rpc","supabase_storage_copy","supabase_storage_create_bucket","supabase_storage_create_signed_upload_url","supabase_storage_create_signed_url","supabase_storage_delete","supabase_storage_delete_bucket","supabase_storage_download","supabase_storage_empty_bucket","supabase_storage_get_public_url","supabase_storage_list","supabase_storage_list_buckets","supabase_storage_move","supabase_storage_update_bucket","supabase_storage_upload","supabase_text_search","supabase_update","supabase_upsert","supabase_vector_search","table_batch_insert_rows","table_create","table_delete_row","table_delete_rows_by_filter","table_get_row","table_get_schema","table_insert_row","table_list","table_query_rows","table_query_rows_v2","table_update_row","table_update_rows_by_filter","table_upsert_row","tailscale_authorize_device","tailscale_create_auth_key","tailscale_delete_auth_key","tailscale_delete_device","tailscale_delete_user","tailscale_expire_device_key","tailscale_get_acl","tailscale_get_auth_key","tailscale_get_device","tailscale_get_device_routes","tailscale_get_dns_preferences","tailscale_get_dns_searchpaths","tailscale_list_auth_keys","tailscale_list_devices","tailscale_list_dns_nameservers","tailscale_list_users","tailscale_set_acl","tailscale_set_device_routes","tailscale_set_device_tags","tailscale_set_dns_nameservers","tailscale_set_dns_preferences","tailscale_set_dns_searchpaths","tailscale_suspend_user","tailscale_update_device_key","tavily_crawl","tavily_extract","tavily_map","tavily_search","telegram_copy_message","telegram_delete_message","telegram_edit_message_text","telegram_forward_message","telegram_get_chat","telegram_get_chat_member","telegram_message","telegram_pin_message","telegram_send_animation","telegram_send_audio","telegram_send_chat_action","telegram_send_contact","telegram_send_document","telegram_send_location","telegram_send_photo","telegram_send_poll","telegram_send_video","telegram_set_message_reaction","telegram_unpin_message","temporal_cancel_workflow","temporal_count_workflows","temporal_create_schedule","temporal_delete_schedule","temporal_describe_schedule","temporal_describe_task_queue","temporal_describe_workflow","temporal_get_workflow_history","temporal_list_schedules","temporal_list_workflows","temporal_pause_schedule","temporal_query_workflow","temporal_reset_workflow","temporal_signal_with_start","temporal_signal_workflow","temporal_start_workflow","temporal_terminate_workflow","temporal_trigger_schedule","temporal_unpause_schedule","temporal_update_workflow","textract_analyze_expense","textract_analyze_id","textract_parser","textract_parser_v2","thinking_tool","thrive_add_audience_managers","thrive_add_audience_members","thrive_add_user_tags","thrive_create_assignment","thrive_create_audience","thrive_create_completion","thrive_create_user","thrive_delete_assignment","thrive_delete_audience","thrive_delete_user","thrive_get_activity","thrive_get_assignment","thrive_get_audience","thrive_get_completion","thrive_get_content","thrive_get_cpd_category","thrive_get_cpd_entry","thrive_get_cpd_requirement","thrive_get_enrolment","thrive_get_skill_levels","thrive_get_tag","thrive_get_user_by_id","thrive_get_user_by_ref","thrive_list_assignments","thrive_list_audience_managers","thrive_list_audience_members","thrive_list_audiences","thrive_list_completions","thrive_list_enrolments","thrive_list_tags","thrive_query_activities","thrive_query_content","thrive_query_cpd_categories","thrive_query_cpd_entries","thrive_query_cpd_requirements","thrive_query_cpd_user_summaries","thrive_remove_audience_manager","thrive_remove_audience_member","thrive_remove_user_tags","thrive_replace_audience_managers","thrive_replace_audience_members","thrive_search_users","thrive_suspend_user","thrive_update_assignment","thrive_update_audience","thrive_update_user","thrive_update_user_skills","tiktok_get_post_status","tiktok_get_user","tiktok_list_videos","tiktok_query_videos","tiktok_upload_video_draft","tinybird_append_datasource","tinybird_delete_datasource_rows","tinybird_events","tinybird_get_job","tinybird_query","tinybird_query_pipe","tinybird_truncate_datasource","tinyfish_cancel_run","tinyfish_fetch","tinyfish_get_run","tinyfish_list_runs","tinyfish_list_vault_items","tinyfish_run","tinyfish_run_async","tinyfish_search","trello_add_checklist","trello_add_checklist_item","trello_add_comment","trello_add_label","trello_add_member","trello_create_board","trello_create_card","trello_create_list","trello_delete_card","trello_get_actions","trello_get_board","trello_get_card","trello_list_cards","trello_list_lists","trello_list_members","trello_remove_label","trello_remove_member","trello_search","trello_update_card","trello_update_checklist_item","trello_update_list","trigger_dev_activate_schedule","trigger_dev_add_run_tags","trigger_dev_batch_trigger_task","trigger_dev_cancel_run","trigger_dev_complete_waitpoint_token","trigger_dev_create_env_var","trigger_dev_create_schedule","trigger_dev_create_waitpoint_token","trigger_dev_deactivate_schedule","trigger_dev_delete_env_var","trigger_dev_delete_schedule","trigger_dev_execute_query","trigger_dev_get_batch","trigger_dev_get_batch_results","trigger_dev_get_deployment","trigger_dev_get_env_var","trigger_dev_get_latest_deployment","trigger_dev_get_query_schema","trigger_dev_get_queue","trigger_dev_get_run","trigger_dev_get_run_events","trigger_dev_get_run_result","trigger_dev_get_run_trace","trigger_dev_get_schedule","trigger_dev_get_waitpoint_token","trigger_dev_import_env_vars","trigger_dev_list_deployments","trigger_dev_list_env_vars","trigger_dev_list_queues","trigger_dev_list_runs","trigger_dev_list_schedules","trigger_dev_list_timezones","trigger_dev_list_waitpoint_tokens","trigger_dev_override_queue_concurrency","trigger_dev_pause_queue","trigger_dev_promote_deployment","trigger_dev_replay_run","trigger_dev_reschedule_run","trigger_dev_reset_queue_concurrency","trigger_dev_resume_queue","trigger_dev_trigger_task","trigger_dev_update_env_var","trigger_dev_update_run_metadata","trigger_dev_update_schedule","tts_azure","tts_cartesia","tts_deepgram","tts_elevenlabs","tts_google","tts_openai","tts_playht","twilio_send_sms","twilio_voice_get_recording","twilio_voice_list_calls","twilio_voice_make_call","typeform_create_form","typeform_delete_form","typeform_files","typeform_get_form","typeform_insights","typeform_list_forms","typeform_responses","typeform_update_form","upstash_redis_command","upstash_redis_delete","upstash_redis_exists","upstash_redis_expire","upstash_redis_get","upstash_redis_hget","upstash_redis_hgetall","upstash_redis_hset","upstash_redis_incr","upstash_redis_incrby","upstash_redis_keys","upstash_redis_lpush","upstash_redis_lrange","upstash_redis_set","upstash_redis_setnx","upstash_redis_ttl","uptimerobot_create_alert_contact","uptimerobot_create_maintenance_window","uptimerobot_create_monitor","uptimerobot_create_psp","uptimerobot_delete_alert_contact","uptimerobot_delete_maintenance_window","uptimerobot_delete_monitor","uptimerobot_delete_psp","uptimerobot_get_account","uptimerobot_get_alert_contact","uptimerobot_get_incident","uptimerobot_get_maintenance_window","uptimerobot_get_monitor","uptimerobot_get_psp","uptimerobot_list_alert_contacts","uptimerobot_list_incidents","uptimerobot_list_maintenance_windows","uptimerobot_list_monitors","uptimerobot_list_psps","uptimerobot_pause_monitor","uptimerobot_start_monitor","uptimerobot_update_maintenance_window","uptimerobot_update_monitor","uptimerobot_update_psp","vanta_download_document_file","vanta_get_control","vanta_get_document","vanta_get_framework","vanta_get_person","vanta_get_policy","vanta_get_risk_scenario","vanta_get_test","vanta_get_vendor","vanta_get_vulnerable_asset","vanta_list_control_documents","vanta_list_control_tests","vanta_list_controls","vanta_list_document_uploads","vanta_list_documents","vanta_list_framework_controls","vanta_list_frameworks","vanta_list_monitored_computers","vanta_list_people","vanta_list_policies","vanta_list_risk_scenarios","vanta_list_test_entities","vanta_list_tests","vanta_list_vendors","vanta_list_vulnerabilities","vanta_list_vulnerability_remediations","vanta_list_vulnerable_assets","vanta_submit_document","vanta_upload_document_file","vercel_add_domain","vercel_add_project_domain","vercel_cancel_deployment","vercel_create_alias","vercel_create_check","vercel_create_deployment","vercel_create_dns_record","vercel_create_edge_config","vercel_create_env_var","vercel_create_project","vercel_create_webhook","vercel_delete_alias","vercel_delete_deployment","vercel_delete_dns_record","vercel_delete_domain","vercel_delete_edge_config","vercel_delete_env_var","vercel_delete_project","vercel_delete_webhook","vercel_get_alias","vercel_get_check","vercel_get_deployment","vercel_get_deployment_events","vercel_get_domain","vercel_get_domain_config","vercel_get_edge_config","vercel_get_edge_config_items","vercel_get_env_vars","vercel_get_project","vercel_get_team","vercel_get_user","vercel_get_webhook","vercel_list_aliases","vercel_list_checks","vercel_list_deployment_files","vercel_list_deployments","vercel_list_dns_records","vercel_list_domains","vercel_list_edge_configs","vercel_list_project_domains","vercel_list_projects","vercel_list_team_members","vercel_list_teams","vercel_list_webhooks","vercel_pause_project","vercel_promote_deployment","vercel_remove_project_domain","vercel_rerequest_check","vercel_unpause_project","vercel_update_check","vercel_update_dns_record","vercel_update_edge_config_items","vercel_update_env_var","vercel_update_project","vercel_update_project_domain","vercel_verify_project_domain","video_falai","video_luma","video_minimax","video_runway","video_veo","vision_tool","vision_tool_v2","wealthbox_read_contact","wealthbox_read_note","wealthbox_read_task","wealthbox_write_contact","wealthbox_write_note","wealthbox_write_task","webflow_create_item","webflow_delete_item","webflow_get_item","webflow_list_items","webflow_update_item","webhook_request","whatsapp_get_media","whatsapp_mark_read","whatsapp_send_interactive","whatsapp_send_media","whatsapp_send_message","whatsapp_send_reaction","whatsapp_send_template","whatsapp_upload_media","wikipedia_content","wikipedia_random","wikipedia_search","wikipedia_summary","windchill_check_in_document","windchill_check_in_documents","windchill_check_out_document","windchill_check_out_documents","windchill_create_document","windchill_create_documents","windchill_delete_document","windchill_delete_documents","windchill_download_attachment","windchill_download_primary_content","windchill_get_document","windchill_get_document_structure","windchill_get_primary_content","windchill_get_valid_state_transitions","windchill_list_attachments","windchill_list_documents","windchill_revise_document","windchill_revise_documents","windchill_set_lifecycle_state","windchill_undo_check_out_document","windchill_undo_check_out_documents","windchill_update_common_properties","windchill_update_document","windchill_update_document_security_labels","windchill_update_documents","windchill_upload_attachments","windchill_upload_primary_content","wiza_company_enrichment","wiza_get_credits","wiza_individual_reveal","wiza_prospect_search","wordpress_create_category","wordpress_create_comment","wordpress_create_page","wordpress_create_post","wordpress_create_tag","wordpress_delete_category","wordpress_delete_comment","wordpress_delete_media","wordpress_delete_page","wordpress_delete_post","wordpress_delete_tag","wordpress_get_category","wordpress_get_current_user","wordpress_get_media","wordpress_get_page","wordpress_get_post","wordpress_get_tag","wordpress_get_user","wordpress_list_categories","wordpress_list_comments","wordpress_list_media","wordpress_list_pages","wordpress_list_posts","wordpress_list_tags","wordpress_list_users","wordpress_search_content","wordpress_update_category","wordpress_update_comment","wordpress_update_page","wordpress_update_post","wordpress_update_tag","wordpress_upload_media","workday_assign_onboarding","workday_change_job","workday_create_prehire","workday_get_compensation","workday_get_organizations","workday_get_worker","workday_hire_employee","workday_list_workers","workday_terminate_worker","workday_update_worker","workflow_executor","x_create_bookmark","x_create_tweet","x_delete_bookmark","x_delete_tweet","x_get_blocking","x_get_bookmarks","x_get_followers","x_get_following","x_get_liked_tweets","x_get_liking_users","x_get_me","x_get_personalized_trends","x_get_quote_tweets","x_get_retweeted_by","x_get_trends_by_woeid","x_get_tweets_by_ids","x_get_usage","x_get_user_mentions","x_get_user_timeline","x_get_user_tweets","x_hide_reply","x_manage_block","x_manage_follow","x_manage_like","x_manage_mute","x_manage_retweet","x_read","x_search","x_search_tweets","x_search_users","x_user","x_write","youtube_channel_info","youtube_channel_playlists","youtube_channel_videos","youtube_comments","youtube_playlist_items","youtube_search","youtube_trending","youtube_video_categories","youtube_video_details","zendesk_autocomplete_organizations","zendesk_create_organization","zendesk_create_organizations_bulk","zendesk_create_ticket","zendesk_create_tickets_bulk","zendesk_create_user","zendesk_create_users_bulk","zendesk_delete_organization","zendesk_delete_ticket","zendesk_delete_user","zendesk_get_current_user","zendesk_get_organization","zendesk_get_organizations","zendesk_get_ticket","zendesk_get_tickets","zendesk_get_user","zendesk_get_users","zendesk_merge_tickets","zendesk_search","zendesk_search_count","zendesk_search_users","zendesk_update_organization","zendesk_update_ticket","zendesk_update_tickets_bulk","zendesk_update_user","zendesk_update_users_bulk","zep_add_messages","zep_add_user","zep_create_thread","zep_delete_thread","zep_get_context","zep_get_messages","zep_get_threads","zep_get_user","zep_get_user_threads","zerobounce_get_credits","zerobounce_verify_email","zoho_desk_add_comment","zoho_desk_get_attachment","zoho_desk_get_contact","zoho_desk_get_thread","zoho_desk_get_ticket","zoho_desk_list_comments","zoho_desk_list_organizations","zoho_desk_list_threads","zoho_desk_list_tickets","zoho_desk_update_ticket","zoom_create_meeting","zoom_delete_meeting","zoom_delete_recording","zoom_get_meeting","zoom_get_meeting_invitation","zoom_get_meeting_recordings","zoom_list_meetings","zoom_list_past_participants","zoom_list_recordings","zoom_update_meeting","zoominfo_enrich_companies","zoominfo_enrich_contacts","zoominfo_search_companies","zoominfo_search_contacts","zoominfo_search_intent","zoominfo_search_news"]' ) export default toolIds diff --git a/apps/sim/tools/generated/tool-metadata.ts b/apps/sim/tools/generated/tool-metadata.ts index a6952f954fe..59bef38f5bd 100644 --- a/apps/sim/tools/generated/tool-metadata.ts +++ b/apps/sim/tools/generated/tool-metadata.ts @@ -3,7 +3,7 @@ /** Serializable metadata for every built-in tool, keyed by tool id. */ const toolMetadata: Record = JSON.parse( - '{"a2a_cancel_task":{"id":"a2a_cancel_task","name":"A2A Cancel Task","description":"Request cancellation of an in-progress A2A task.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The task ID to cancel"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}},"hostedApiKey":"none"},"a2a_get_agent_card":{"id":"a2a_get_agent_card","name":"A2A Get Agent Card","description":"Fetch the Agent Card (discovery document) for an external A2A agent.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}},"hostedApiKey":"none"},"a2a_get_task":{"id":"a2a_get_task","name":"A2A Get Task","description":"Retrieve the current state and result of an A2A task.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The task ID to retrieve"},"historyLength":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of history messages to include"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}},"hostedApiKey":"none"},"a2a_send_message":{"id":"a2a_send_message","name":"A2A Send Message","description":"Send a message to an external A2A agent and return its response.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"message":{"type":"string","required":true,"visibility":"user-or-llm","description":"The message text to send"},"data":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional structured JSON data to attach"},"files":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional files to attach"},"taskId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Existing task ID to continue"},"contextId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Conversation context ID to continue"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}},"hostedApiKey":"none"},"affinity_batch_update_entity_fields":{"id":"affinity_batch_update_entity_fields","name":"Affinity Batch Update Entity Fields","description":"Write up to 100 non-list field values on one company or person in a single request.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to write the fields on: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"updates":{"type":"json","required":true,"visibility":"user-or-llm","description":"Up to 100 field updates as [{\\"id\\":\\"\\",\\"value\\":{\\"type\\":\\"…\\",\\"data\\":…}}], using the same value shapes as a single field update"}},"hostedApiKey":"none"},"affinity_batch_update_list_entry_fields":{"id":"affinity_batch_update_list_entry_fields","name":"Affinity Batch Update List Entry Fields","description":"Write up to 100 field values on one list row in a single request. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"updates":{"type":"json","required":true,"visibility":"user-or-llm","description":"Up to 100 field updates as [{\\"id\\":\\"\\",\\"value\\":{\\"type\\":\\"…\\",\\"data\\":…}}], using the same value shapes as a single field update"}},"hostedApiKey":"none"},"affinity_create_list":{"id":"affinity_create_list","name":"Affinity Create List","description":"Create a list. Its type fixes which entities it can hold, and the API key holder becomes its creator and owner.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the new list"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Entity kind the list holds: company, opportunity, or person"},"isPublic":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether everyone in the organization can see the list"}},"hostedApiKey":"none"},"affinity_create_list_field_dropdown_option":{"id":"affinity_create_list_field_dropdown_option","name":"Affinity Create List Field Dropdown Option","description":"Add a selectable option to a dropdown field on a list. A ranked or status option also needs a rank and a color.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Kind of option to create, matching the field. dropdown takes only a label; ranked-dropdown also requires rank and color; status-dropdown additionally requires a status category. Sending a field the kind does not accept is rejected"},"text":{"type":"string","required":true,"visibility":"user-or-llm","description":"The option label"},"rank":{"type":"number","required":false,"visibility":"user-or-llm","description":"Sort order. Required on a ranked-dropdown or status-dropdown option"},"color":{"type":"string","required":false,"visibility":"user-or-llm","description":"Option color: white, gray, blue, green, purple, orange, or red. Required on a ranked-dropdown or status-dropdown option"},"statusCategory":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pipeline meaning of the option: open, won, lost, or on-hold. Status-dropdown options only"},"winRate":{"type":"number","required":false,"visibility":"user-or-llm","description":"Expected win rate of the status. Status-dropdown options only"}},"hostedApiKey":"none"},"affinity_create_merge":{"id":"affinity_create_merge","name":"Affinity Create Merge","description":"Fold a duplicate company or person into the record you are keeping. The merge runs asynchronously — poll the returned task to see it finish. Requires the \\"Manage duplicates\\" permission and an admin role.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to merge: companies or persons"},"primaryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to keep"},"duplicateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the duplicate record to fold in"}},"hostedApiKey":"none"},"affinity_create_note":{"id":"affinity_create_note","name":"Affinity Create Note","description":"Write a note — attached to companies, persons, and opportunities, anchored to a meeting, call, or chat message, or posted as a reply to an existing note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Note shape: entities to attach it to records, interaction to anchor it to a meeting, call, or chat message, or user-reply to reply to a note"},"html":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note body as HTML"},"companyIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Companies to attach the note to, e.g. [1, 2]. Not used on a reply"},"personIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Persons to attach the note to, e.g. [1, 2]. Not used on a reply"},"opportunityIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Opportunities to attach the note to, e.g. [1, 2]. Not used on a reply"},"interactionId":{"type":"string","required":false,"visibility":"user-or-llm","description":"The interaction to anchor the note to. Required for an interaction note"},"interactionType":{"type":"string","required":false,"visibility":"user-or-llm","description":"Kind of the anchoring interaction: meeting, call, or chat-message. Required for an interaction note"},"parentId":{"type":"string","required":false,"visibility":"user-or-llm","description":"The note being replied to. Required for a user-reply note"},"creatorId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Attribute the note to another internal person. Defaults to the API key holder"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"Backdate the note to this ISO 8601 timestamp"}},"hostedApiKey":"none"},"affinity_create_reminder":{"id":"affinity_create_reminder","name":"Affinity Create Reminder","description":"Create a reminder on one company, person, or opportunity. A recurring reminder resets whenever the chosen signal happens instead of firing once.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"one-time to fire once, or recurring to reset on a signal"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"What the reminder is about: company, person, or opportunity"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company, person, or opportunity"},"dueDate":{"type":"string","required":false,"visibility":"user-or-llm","description":"When the reminder is due, as an ISO 8601 timestamp. Required for a one-time reminder; on a recurring one Affinity computes it from the period when omitted"},"content":{"type":"string","required":false,"visibility":"user-or-llm","description":"What the reminder says"},"ownerId":{"type":"string","required":true,"visibility":"user-or-llm","description":"User the reminder is assigned to. Must be an internal user. The API key holder is recorded as the creator, which is a separate field"},"resetTrigger":{"type":"string","required":false,"visibility":"user-or-llm","description":"What restarts a recurring reminder: interaction, email, or event. Required when the type is recurring"},"periodDays":{"type":"number","required":false,"visibility":"user-or-llm","description":"Days between firings of a recurring reminder. Required when the type is recurring"}},"hostedApiKey":"none"},"affinity_delete_list_field_dropdown_option":{"id":"affinity_delete_list_field_dropdown_option","name":"Affinity Delete List Field Dropdown Option","description":"Permanently delete a dropdown option on a list field. Every list entry currently set to it is cleared, and those values cannot be recovered.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID to delete"}},"hostedApiKey":"none"},"affinity_delete_note":{"id":"affinity_delete_note","name":"Affinity Delete Note","description":"Delete a note you created. Deleting a root note also deletes its replies; deleting a reply removes only that reply.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID to delete"}},"hostedApiKey":"none"},"affinity_get_company":{"id":"affinity_get_company","name":"Affinity Get Company","description":"Look up one company by ID. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"companyId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The company ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_current_user":{"id":"affinity_get_current_user","name":"Affinity Get Current User","description":"Verify an Affinity API key and return the tenant, the user behind the key, and the scopes the grant carries.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"}},"hostedApiKey":"none"},"affinity_get_entity_field_value":{"id":"affinity_get_entity_field_value","name":"Affinity Get Entity Field Value","description":"Read one non-list field value from a company or person.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to read the field from: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to read"}},"hostedApiKey":"none"},"affinity_get_list":{"id":"affinity_get_list","name":"Affinity Get List","description":"Read one list — its name, type, owner, and privacy setting.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"}},"hostedApiKey":"none"},"affinity_get_list_entry":{"id":"affinity_get_list_entry","name":"Affinity Get List Entry","description":"Read one row of a list with its entity. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_list_entry_field":{"id":"affinity_get_list_entry_field","name":"Affinity Get List Entry Field","description":"Read one field value on a list row.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to read"}},"hostedApiKey":"none"},"affinity_get_list_field_dropdown_option":{"id":"affinity_get_list_field_dropdown_option","name":"Affinity Get List Field Dropdown Option","description":"Read one dropdown option on a list field.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID"}},"hostedApiKey":"none"},"affinity_get_merge":{"id":"affinity_get_merge","name":"Affinity Get Merge","description":"Read the status of one company or person merge, including why it failed if it did.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge to read: companies or persons"},"mergeId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The merge ID"}},"hostedApiKey":"none"},"affinity_get_merge_task":{"id":"affinity_get_merge_task","name":"Affinity Get Merge Task","description":"Read one merge task and how its merges are progressing. Poll this after starting a merge.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge task to read: companies or persons"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The merge task ID"}},"hostedApiKey":"none"},"affinity_get_note":{"id":"affinity_get_note","name":"Affinity Get Note","description":"Read one note with its body, author, mentions, and attached records.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return, e.g. [\\"repliesCount\\",\\"personsPreview\\",\\"companiesPreview\\",\\"opportunitiesPreview\\"]. Those four fields are omitted unless requested here"}},"hostedApiKey":"none"},"affinity_get_opportunity":{"id":"affinity_get_opportunity","name":"Affinity Get Opportunity","description":"Read one opportunity and the list it belongs to. Its field data lives on the list entry.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"opportunityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The opportunity ID"}},"hostedApiKey":"none"},"affinity_get_person":{"id":"affinity_get_person","name":"Affinity Get Person","description":"Look up one person by ID. Field data is returned only for the Field IDs or Field Types asked for.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"personId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The person ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"}},"hostedApiKey":"none"},"affinity_get_saved_view":{"id":"affinity_get_saved_view","name":"Affinity Get Saved View","description":"Read one saved view — its name, kind, and creation date.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"viewId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The saved view ID"}},"hostedApiKey":"none"},"affinity_get_transcript":{"id":"affinity_get_transcript","name":"Affinity Get Transcript","description":"Read one transcript with its first 100 fragments. Page the fragments endpoint for a longer meeting.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"transcriptId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The transcript ID"}},"hostedApiKey":"none"},"affinity_get_user":{"id":"affinity_get_user","name":"Affinity Get User","description":"Read one internal user. A user and their person record share the same numeric ID, so a person ID works here.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"userId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The user ID, which is also their person ID"}},"hostedApiKey":"none"},"affinity_list_calls":{"id":"affinity_list_calls","name":"Affinity List Calls","description":"Page through logged calls and their participants. Only calls the API key holder can see are returned.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_chat_messages":{"id":"affinity_list_chat_messages","name":"Affinity List Chat Messages","description":"Page through logged chat messages and their participants. Only messages the API key holder can see are returned.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_companies":{"id":"affinity_list_companies","name":"Affinity List Companies","description":"Page through companies. Companies come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these company IDs, e.g. [1, 2, 3]"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_coworker_connections":{"id":"affinity_list_coworker_connections","name":"Affinity List Coworker Connections","description":"Find warm paths into a company through shared work history: who in your Affinity data once worked alongside the people you want to reach. Grouped by target, strongest first.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":true,"visibility":"user-or-llm","description":"Required scope. The only supported filter is target.currentCompany.id, e.g. \\"target.currentCompany.id=123\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of targets to return per page, 1-50. Defaults to 20"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_emails":{"id":"affinity_list_emails","name":"Affinity List Emails","description":"Page through email metadata — subject, participants, and timestamps. Affinity never exposes email bodies through the API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_field_values":{"id":"affinity_list_entity_field_values","name":"Affinity List Entity Field Values","description":"Page through a company\'s or person\'s non-list field values. List fields are not returned here — read those through the list entry.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to read field values from: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field IDs. Mutually exclusive with Field Types"},"types":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field categories: enriched, global, relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_list_entity_list_entries":{"id":"affinity_list_entity_list_entries","name":"Affinity List Entity List Entries","description":"Page through a company\'s or person\'s rows across every list, each carrying that list\'s field values and when the entity was added.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up the rows of: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_lists":{"id":"affinity_list_entity_lists","name":"Affinity List Entity Lists","description":"List every list a company or person appears on that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up the lists of: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_entity_notes":{"id":"affinity_list_entity_notes","name":"Affinity List Entity Notes","description":"List the notes relevant to one company, person, or opportunity — directly attached notes plus notes reaching it through its people and meetings.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity the notes hang off: companies, persons, or opportunities"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company, person, or opportunity"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_entity_relationships":{"id":"affinity_list_entity_relationships","name":"Affinity List Entity Relationships","description":"List who knows a company or person, scored 0.0 to 1.0 by how much the two actually interact. Strongest first by default.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to look up relationships for: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on interactionScore only, e.g. \\"interactionScore>=0.5\\""},"orderBy":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order: [\\"interactionScore\\"] for weakest first, [\\"-interactionScore\\"] for strongest first (the default)"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_field_dropdown_options":{"id":"affinity_list_field_dropdown_options","name":"Affinity List Field Dropdown Options","description":"List the selectable options on a dropdown or ranked-dropdown company or person field. Writing such a field needs the option ID, not its text.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which field family the field belongs to: companies or persons"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown or ranked-dropdown field ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_field_metadata":{"id":"affinity_list_field_metadata","name":"Affinity List Field Metadata","description":"List the non-list company or person fields, with the value type, filter operators, and sort support of each. Start here to find the Field IDs the read and write tools take.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which fields to describe: companies or persons"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return: [\\"filterability\\",\\"sortability\\"]. Both are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on name only, e.g. \\"name=~Status\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_field_value_changes":{"id":"affinity_list_field_value_changes","name":"Affinity List Field Value Changes","description":"Page through field value changes across the whole workspace. Built for delta sync: follow nextCursor to the end of a run, then resume from the last cursor next time.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over field.id, listEntry.id, changer.id, changedAt, or actionType. Resume a sync with e.g. \\"changedAt>2026-06-01T12:00:00Z\\""},"orderBy":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order: [\\"changedAt\\"] for oldest first (the default), [\\"-changedAt\\"] for newest first"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_investor_executive_connections":{"id":"affinity_list_investor_executive_connections","name":"Affinity List Investor Executive Connections","description":"Find warm paths into a company through investment history: which investors in your Affinity data backed a company the people you want to reach once led. Grouped by target, strongest first.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":true,"visibility":"user-or-llm","description":"Required scope. The only supported filter is target.currentCompany.id, e.g. \\"target.currentCompany.id=123\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of targets to return per page, 1-50. Defaults to 20"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_list_entries":{"id":"affinity_list_list_entries","name":"Affinity List List Entries","description":"Page through the rows of a list. Rows come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_entry_field_value_changes":{"id":"affinity_list_list_entry_field_value_changes","name":"Affinity List List Entry Field Value Changes","description":"Page through the history of one list row — who changed which field, when, and to what.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over field.id, changer.id, changedAt, or actionType, e.g. \\"field.id=field-1234\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_entry_fields":{"id":"affinity_list_list_entry_fields","name":"Affinity List List Entry Fields","description":"Page through every field value on one list row, including the list-specific columns. All fields are returned unless narrowed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field IDs. Mutually exclusive with Field Types"},"types":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict to these field categories: enriched, global, list, relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_list_list_field_dropdown_options":{"id":"affinity_list_list_field_dropdown_options","name":"Affinity List List Field Dropdown Options","description":"List the selectable options on a dropdown, ranked-dropdown, or status-dropdown field of a list.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_list_fields":{"id":"affinity_list_list_fields","name":"Affinity List List Fields","description":"List the fields available on one list, including its list-specific columns. Use these Field IDs when reading or writing list entries.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return: [\\"filterability\\",\\"sortability\\"]. Both are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on name only, e.g. \\"name=~Stage\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_lists":{"id":"affinity_list_lists","name":"Affinity List Lists","description":"Page through the lists in the organization that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"term":{"type":"string","required":false,"visibility":"user-or-llm","description":"Case-insensitive substring match on the list name"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_meetings":{"id":"affinity_list_meetings","name":"Affinity List Meetings","description":"Page through past and upcoming meetings with their organizer and attendees.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_merge_tasks":{"id":"affinity_list_merge_tasks","name":"Affinity List Merge Tasks","description":"Page through merge tasks, each summarizing how many of its merges are in progress, succeeded, or failed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merge tasks to list: companies or persons"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression. This endpoint filters on status only, e.g. \\"status=in-progress\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_merges":{"id":"affinity_list_merges","name":"Affinity List Merges","description":"Page through the company or person merges the organization has run, with the status and the records involved in each.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which merges to list: companies or persons"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over status or taskId, e.g. \\"status=failed\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_note_attached_companies":{"id":"affinity_list_note_attached_companies","name":"Affinity List Note Attached Companies","description":"List the companies directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_attached_opportunities":{"id":"affinity_list_note_attached_opportunities","name":"Affinity List Note Attached Opportunities","description":"List the opportunities directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_attached_persons":{"id":"affinity_list_note_attached_persons","name":"Affinity List Note Attached Persons","description":"List the persons directly attached to one note.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_note_replies":{"id":"affinity_list_note_replies","name":"Affinity List Note Replies","description":"Page through the replies on one note, including AI Notetaker replies.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID whose replies to read"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_notes":{"id":"affinity_list_notes","name":"Affinity List Notes","description":"Page through every note the caller can see. Replies are excluded.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"includes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Extra properties to return, e.g. [\\"repliesCount\\",\\"personsPreview\\",\\"companiesPreview\\",\\"opportunitiesPreview\\"]. Those four fields are omitted unless requested here"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_opportunities":{"id":"affinity_list_opportunities","name":"Affinity List Opportunities","description":"Page through opportunities. Field data lives on the list entry, not here — read it through the list or saved view the opportunity belongs to.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these opportunity IDs, e.g. [1, 2, 3]"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_persons":{"id":"affinity_list_persons","name":"Affinity List Persons","description":"Page through persons. Persons come back without field data unless Field IDs or Field Types asks for it.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the page to these person IDs, e.g. [1, 2, 3]"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_reminders":{"id":"affinity_list_reminders","name":"Affinity List Reminders","description":"Page through the reminders the caller can see. Filter by status to surface what is overdue.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_saved_view_entries":{"id":"affinity_list_saved_view_entries","name":"Affinity List Saved View Entries","description":"Page through the rows of a saved view. The view\'s own filters and columns decide which rows and which field data come back.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"viewId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The saved view ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_saved_views":{"id":"affinity_list_saved_views","name":"Affinity List Saved Views","description":"List the saved views on a list that the caller can view.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_list_transcript_fragments":{"id":"affinity_list_transcript_fragments","name":"Affinity List Transcript Fragments","description":"Page through everything said in a meeting, segment by segment with the speaker.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"transcriptId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The transcript ID"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_transcripts":{"id":"affinity_list_transcripts","name":"Affinity List Transcripts","description":"Page through meeting transcript metadata. Read one transcript to get what was actually said.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression, e.g. \\"createdAt>=2026-01-01\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_list_users":{"id":"affinity_list_users","name":"Affinity List Users","description":"Page through the internal users in the organization. Email addresses and roles are returned only to callers with the \\"Manage Users\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"term":{"type":"string","required":false,"visibility":"user-or-llm","description":"Case-insensitive match across first name, last name, and primary email"},"filter":{"type":"string","required":false,"visibility":"user-or-llm","description":"Affinity Filtering Language expression over id or status, e.g. \\"status=active\\""},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_search_companies":{"id":"affinity_search_companies","name":"Affinity Search Companies","description":"Search companies by filters, sorts, and a free-text term. Requires the \\"Export All Organizations directory\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_search_files":{"id":"affinity_search_files","name":"Affinity Search Files","description":"Search files by keyword, ordered by relevance. Narrow to specific files or to one company, or leave both unset to search the whole account.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to search for. Between 3 and 500 characters"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to these file IDs. Cannot be combined with Company ID"},"companyId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Restrict the search to one company\'s files. Cannot be combined with file IDs"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of files to return, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_search_list_entries":{"id":"affinity_search_list_entries","name":"Affinity Search List Entries","description":"Search the rows of one list by filters, sorts, and a free-text term. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID to search"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, list, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_search_notes":{"id":"affinity_search_notes","name":"Affinity Search Notes","description":"Search notes by keyword, ordered by relevance. Narrow to specific notes or to one company, or leave both unset to search the whole account.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to search for. Between 3 and 500 characters"},"ids":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to these note IDs. Cannot be combined with Company ID"},"companyId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Restrict the search to one company\'s notes. Cannot be combined with note IDs"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of notes to return, 1-100. Defaults to 20"}},"hostedApiKey":"none"},"affinity_search_persons":{"id":"affinity_search_persons","name":"Affinity Search Persons","description":"Search persons by filters, sorts, and a free-text term. Requires the \\"Export All People directory\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"filters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Filter group as {operator: \\"and\\"|\\"or\\", filters: [...]}, at most 50 leaves. Each leaf is {valueType, fieldId, operator, value}, and a leaf may itself be a nested group"},"searchTerm":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-text term matched against the searchable fields. At least 3 characters"},"searchFieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs the search term is matched against. Defaults to the searchable fields"},"sorts":{"type":"json","required":false,"visibility":"user-or-llm","description":"Sort order as [{fieldId, direction: \\"asc\\"|\\"desc\\", attributeId?}], up to 5, applied in order"},"fieldIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field IDs to return values for, e.g. [\\"affinity-data-location\\"]. Mutually exclusive with Field Types"},"fieldTypes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Field categories to return values for: enriched, global, or relationship-intelligence. Mutually exclusive with Field IDs"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous page, returned as nextCursor or prevCursor"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to return per page, 1-100. Defaults to 100"},"totalCount":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the total size of the collection. Costs an extra query"}},"hostedApiKey":"none"},"affinity_semantic_search":{"id":"affinity_semantic_search","name":"Affinity Semantic Search","description":"Find companies from a description in plain language — industry, technology, stage, or business model. Currently searches companies only.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"prompt":{"type":"string","required":true,"visibility":"user-or-llm","description":"What to look for, in plain language, e.g. \\"climate tech companies in our pipeline\\". Up to 500 characters"},"listIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Restrict the search to companies on these lists, e.g. [1, 2]"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of companies to return, 1-100. Defaults to 100"}},"hostedApiKey":"none"},"affinity_update_entity_field_value":{"id":"affinity_update_entity_field_value","name":"Affinity Update Entity Field Value","description":"Write one non-list field value on a company or person. The value type must match how the field is defined.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"entityType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Which entity to write the field on: companies or persons"},"entityId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of that company or person"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to write"},"value":{"type":"json","required":true,"visibility":"user-or-llm","description":"The new value as {type, data}, where type matches the field\'s value type. Examples: {\\"type\\":\\"text\\",\\"data\\":\\"Series B\\"}, {\\"type\\":\\"number\\",\\"data\\":42}, {\\"type\\":\\"dropdown\\",\\"data\\":{\\"dropdownOptionId\\":7}}, {\\"type\\":\\"person\\",\\"data\\":{\\"id\\":123}}, {\\"type\\":\\"person-multi\\",\\"data\\":[{\\"id\\":123}]}. Pass data as null to clear the field"}},"hostedApiKey":"none"},"affinity_update_list_entry_field":{"id":"affinity_update_list_entry_field","name":"Affinity Update List Entry Field","description":"Write one field value on a list row. Requires the \\"Export data from Lists\\" permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"listEntryId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list entry ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The field ID to write"},"value":{"type":"json","required":true,"visibility":"user-or-llm","description":"The new value as {type, data}, where type matches the field\'s value type. Examples: {\\"type\\":\\"text\\",\\"data\\":\\"Series B\\"}, {\\"type\\":\\"number\\",\\"data\\":42}, {\\"type\\":\\"dropdown\\",\\"data\\":{\\"dropdownOptionId\\":7}}, {\\"type\\":\\"person\\",\\"data\\":{\\"id\\":123}}, {\\"type\\":\\"person-multi\\",\\"data\\":[{\\"id\\":123}]}. Pass data as null to clear the field"}},"hostedApiKey":"none"},"affinity_update_list_field_dropdown_option":{"id":"affinity_update_list_field_dropdown_option","name":"Affinity Update List Field Dropdown Option","description":"Change a dropdown option on a list field. Every field is optional — supply only what should change, and only fields the option\'s kind actually has.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"listId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The list ID"},"fieldId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown field ID on that list"},"dropdownOptionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The dropdown option ID to update"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Replacement option label. Supply at least one field to change"},"rank":{"type":"number","required":false,"visibility":"user-or-llm","description":"Sort order. Required on a ranked-dropdown or status-dropdown option"},"color":{"type":"string","required":false,"visibility":"user-or-llm","description":"Option color: white, gray, blue, green, purple, orange, or red. Required on a ranked-dropdown or status-dropdown option"},"statusCategory":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pipeline meaning of the option: open, won, lost, or on-hold. Status-dropdown options only"},"winRate":{"type":"number","required":false,"visibility":"user-or-llm","description":"Expected win rate of the status. Status-dropdown options only"}},"hostedApiKey":"none"},"affinity_update_note":{"id":"affinity_update_note","name":"Affinity Update Note","description":"Rewrite a note\'s body or replace which records it is attached to. Each list of IDs replaces that association wholesale, an empty list clears it, and omitting one leaves it untouched. A note\'s type cannot be changed.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Affinity API key, sent as a bearer token"},"noteId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note ID to update"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"Replacement note body as HTML"},"companyIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached companies, e.g. [1, 2]. Send [] to detach every company; omit to leave them unchanged"},"personIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached persons, e.g. [1, 2]. Send [] to detach every person; omit to leave them unchanged"},"opportunityIds":{"type":"json","required":false,"visibility":"user-or-llm","description":"Replacement set of attached opportunities, e.g. [1, 2]. Send [] to detach every opportunity; omit to leave them unchanged"}},"hostedApiKey":"none"},"agentmail_create_draft":{"id":"agentmail_create_draft","name":"Create Draft","description":"Create a new email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to create the draft in"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Draft subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text draft body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML draft body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"},"inReplyTo":{"type":"string","required":false,"visibility":"user-or-llm","description":"ID of message being replied to"},"sendAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to schedule sending"}},"hostedApiKey":"none"},"agentmail_create_inbox":{"id":"agentmail_create_inbox","name":"Create Inbox","description":"Create a new email inbox with AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"username":{"type":"string","required":false,"visibility":"user-or-llm","description":"Username for the inbox email address"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Domain for the inbox email address"},"displayName":{"type":"string","required":false,"visibility":"user-or-llm","description":"Display name for the inbox"}},"hostedApiKey":"none"},"agentmail_delete_draft":{"id":"agentmail_delete_draft","name":"Delete Draft","description":"Delete an email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to delete"}},"hostedApiKey":"none"},"agentmail_delete_inbox":{"id":"agentmail_delete_inbox","name":"Delete Inbox","description":"Delete an email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to delete"}},"hostedApiKey":"none"},"agentmail_delete_thread":{"id":"agentmail_delete_thread","name":"Delete Thread","description":"Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to delete"},"permanent":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Force permanent deletion instead of moving to trash"}},"hostedApiKey":"none"},"agentmail_forward_message":{"id":"agentmail_forward_message","name":"Forward Message","description":"Forward an email message to new recipients in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to forward"},"to":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Override subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Additional plain text to prepend"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"Additional HTML to prepend"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"}},"hostedApiKey":"none"},"agentmail_get_draft":{"id":"agentmail_get_draft","name":"Get Draft","description":"Get details of a specific email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox the draft belongs to"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to retrieve"}},"hostedApiKey":"none"},"agentmail_get_inbox":{"id":"agentmail_get_inbox","name":"Get Inbox","description":"Get details of a specific email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to retrieve"}},"hostedApiKey":"none"},"agentmail_get_message":{"id":"agentmail_get_message","name":"Get Message","description":"Get details of a specific email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to retrieve"}},"hostedApiKey":"none"},"agentmail_get_thread":{"id":"agentmail_get_thread","name":"Get Thread","description":"Get details of a specific email thread including messages in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to retrieve"}},"hostedApiKey":"none"},"agentmail_list_drafts":{"id":"agentmail_list_drafts","name":"List Drafts","description":"List email drafts in an inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list drafts from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of drafts to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}},"hostedApiKey":"none"},"agentmail_list_inboxes":{"id":"agentmail_list_inboxes","name":"List Inboxes","description":"List all email inboxes in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of inboxes to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}},"hostedApiKey":"none"},"agentmail_list_messages":{"id":"agentmail_list_messages","name":"List Messages","description":"List messages in an inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list messages from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of messages to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}},"hostedApiKey":"none"},"agentmail_list_threads":{"id":"agentmail_list_threads","name":"List Threads","description":"List email threads in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list threads from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of threads to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"},"labels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to filter threads by"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter threads before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter threads after this ISO 8601 timestamp"}},"hostedApiKey":"none"},"agentmail_reply_message":{"id":"agentmail_reply_message","name":"Reply to Message","description":"Reply to an existing email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to reply from"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to reply to"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text reply body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML reply body"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Override recipient email addresses (comma-separated)"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC email addresses (comma-separated)"},"replyAll":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Reply to all recipients of the original message"}},"hostedApiKey":"none"},"agentmail_send_draft":{"id":"agentmail_send_draft","name":"Send Draft","description":"Send an existing email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to send"}},"hostedApiKey":"none"},"agentmail_send_message":{"id":"agentmail_send_message","name":"Send Message","description":"Send an email message from an AgentMail inbox","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to send from"},"to":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient email address (comma-separated for multiple)"},"subject":{"type":"string","required":true,"visibility":"user-or-llm","description":"Email subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text email body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML email body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"}},"hostedApiKey":"none"},"agentmail_update_draft":{"id":"agentmail_update_draft","name":"Update Draft","description":"Update an existing email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to update"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Draft subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text draft body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML draft body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"},"sendAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to schedule sending"}},"hostedApiKey":"none"},"agentmail_update_inbox":{"id":"agentmail_update_inbox","name":"Update Inbox","description":"Update the display name of an email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to update"},"displayName":{"type":"string","required":true,"visibility":"user-or-llm","description":"New display name for the inbox"}},"hostedApiKey":"none"},"agentmail_update_message":{"id":"agentmail_update_message","name":"Update Message","description":"Add or remove labels on an email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to update"},"addLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to add to the message"},"removeLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to remove from the message"}},"hostedApiKey":"none"},"agentmail_update_thread":{"id":"agentmail_update_thread","name":"Update Thread Labels","description":"Add or remove labels on an email thread in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to update"},"addLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to add to the thread"},"removeLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to remove from the thread"}},"hostedApiKey":"none"},"agentphone_create_call":{"id":"agentphone_create_call","name":"Create Outbound Call","description":"Initiate an outbound voice call from an AgentPhone agent","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"agentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Agent that will handle the call"},"toNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Phone number to call in E.164 format (e.g. +14155551234)"},"fromNumberId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Phone number ID to use as caller ID. Must belong to the agent. If omitted, the agent\'s first assigned number is used."},"initialGreeting":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optional greeting spoken when the recipient answers"},"voice":{"type":"string","required":false,"visibility":"user-or-llm","description":"Voice ID override for this call (defaults to the agent\'s configured voice)"},"systemPrompt":{"type":"string","required":false,"visibility":"user-or-llm","description":"When provided, uses a built-in LLM for the conversation instead of forwarding to your webhook"}},"hostedApiKey":"none"},"agentphone_create_contact":{"id":"agentphone_create_contact","name":"Create Contact","description":"Create a new contact in AgentPhone","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"phoneNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Phone number in E.164 format (e.g. +14155551234)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact\'s full name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Contact\'s email address"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Freeform notes stored on the contact"}},"hostedApiKey":"none"},"agentphone_create_number":{"id":"agentphone_create_number","name":"Create Phone Number","description":"Provision a new SMS- and voice-enabled phone number","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Two-letter country code (e.g. US, CA). Defaults to US."},"areaCode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Preferred area code (US/CA only, e.g. \\"415\\"). Best-effort — may be ignored if unavailable."},"agentId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optionally attach the number to an agent immediately"}},"hostedApiKey":"none"},"agentphone_delete_contact":{"id":"agentphone_delete_contact","name":"Delete Contact","description":"Delete a contact by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"}},"hostedApiKey":"none"},"agentphone_get_call":{"id":"agentphone_get_call","name":"Get Call","description":"Fetch a call and its full transcript","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"callId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the call to retrieve"}},"hostedApiKey":"none"},"agentphone_get_call_transcript":{"id":"agentphone_get_call_transcript","name":"Get Call Transcript","description":"Get the full ordered transcript for a call","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"callId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the call to retrieve the transcript for"}},"hostedApiKey":"none"},"agentphone_get_contact":{"id":"agentphone_get_contact","name":"Get Contact","description":"Fetch a single contact by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"}},"hostedApiKey":"none"},"agentphone_get_conversation":{"id":"agentphone_get_conversation","name":"Get Conversation","description":"Get a conversation along with its recent messages","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"messageLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of recent messages to include (default 50, max 100)"}},"hostedApiKey":"none"},"agentphone_get_conversation_messages":{"id":"agentphone_get_conversation_messages","name":"Get Conversation Messages","description":"Get paginated messages for a conversation","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of messages to return (default 50, max 200)"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received after this ISO 8601 timestamp"}},"hostedApiKey":"none"},"agentphone_get_number_messages":{"id":"agentphone_get_number_messages","name":"Get Phone Number Messages","description":"Fetch messages received on a specific phone number","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"numberId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the phone number"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of messages to return (default 50, max 200)"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received after this ISO 8601 timestamp"}},"hostedApiKey":"none"},"agentphone_get_usage":{"id":"agentphone_get_usage","name":"Get Usage","description":"Retrieve current usage statistics for the AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"}},"hostedApiKey":"none"},"agentphone_get_usage_daily":{"id":"agentphone_get_usage_daily","name":"Get Daily Usage","description":"Get a daily breakdown of usage (messages, calls, webhooks) for the last N days","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"days":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of days to return (1-365, default 30)"}},"hostedApiKey":"none"},"agentphone_get_usage_monthly":{"id":"agentphone_get_usage_monthly","name":"Get Monthly Usage","description":"Get monthly usage aggregation (messages, calls, webhooks) for the last N months","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"months":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of months to return (1-24, default 6)"}},"hostedApiKey":"none"},"agentphone_list_calls":{"id":"agentphone_list_calls","name":"List Calls","description":"List voice calls for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"},"status":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by status (completed, in-progress, failed)"},"direction":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by direction (inbound, outbound)"},"type":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by call type (pstn, web)"},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search by phone number (matches fromNumber or toNumber)"}},"hostedApiKey":"none"},"agentphone_list_contacts":{"id":"agentphone_list_contacts","name":"List Contacts","description":"List contacts for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by name or phone number (case-insensitive contains)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 50, max 200)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}},"hostedApiKey":"none"},"agentphone_list_conversations":{"id":"agentphone_list_conversations","name":"List Conversations","description":"List conversations (message threads) for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}},"hostedApiKey":"none"},"agentphone_list_numbers":{"id":"agentphone_list_numbers","name":"List Phone Numbers","description":"List all phone numbers provisioned for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}},"hostedApiKey":"none"},"agentphone_react_to_message":{"id":"agentphone_react_to_message","name":"React to Message","description":"Send an iMessage tapback reaction to a message (iMessage only)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to react to"},"reaction":{"type":"string","required":true,"visibility":"user-or-llm","description":"Reaction type: love, like, dislike, laugh, emphasize, or question"}},"hostedApiKey":"none"},"agentphone_release_number":{"id":"agentphone_release_number","name":"Release Phone Number","description":"Release (delete) a phone number. This action is irreversible.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"numberId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the phone number to release"}},"hostedApiKey":"none"},"agentphone_send_message":{"id":"agentphone_send_message","name":"Send Message","description":"Send an outbound SMS or iMessage from an AgentPhone agent","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"agentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Agent sending the message"},"toNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient phone number in E.164 format (e.g. +14155551234)"},"body":{"type":"string","required":true,"visibility":"user-or-llm","description":"Message text to send"},"mediaUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optional URL of an image, video, or file to attach"},"numberId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Phone number ID to send from. If omitted, the agent\'s first assigned number is used."}},"hostedApiKey":"none"},"agentphone_update_contact":{"id":"agentphone_update_contact","name":"Update Contact","description":"Update a contact\'s fields","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"},"phoneNumber":{"type":"string","required":false,"visibility":"user-or-llm","description":"New phone number in E.164 format"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New contact name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"New email address"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"New freeform notes"}},"hostedApiKey":"none"},"agentphone_update_conversation":{"id":"agentphone_update_conversation","name":"Update Conversation","description":"Update conversation metadata (stored state). Pass null to clear existing metadata.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"metadata":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom key-value metadata to store on the conversation. Pass null to clear existing metadata."}},"hostedApiKey":"none"},"agiloft_async_status":{"id":"agiloft_async_status","name":"Agiloft Async Status","description":"Check whether an asynchronous Agiloft call, such as a run action button, has completed.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table the asynchronous call was made against"},"callbackId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Callback ID returned by the asynchronous call, e.g. from Run Action Button"}},"hostedApiKey":"none"},"agiloft_attach_file":{"id":"agiloft_attach_file","name":"Agiloft Attach File","description":"Attach a file to a field in an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to attach the file to"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"file":{"type":"file","required":true,"visibility":"user-or-llm","description":"File to attach"},"fileName":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name to assign to the file (defaults to original file name)"},"overwrite":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Replace the contents of the field instead of adding another file to it"}},"hostedApiKey":"none"},"agiloft_attachment_info":{"id":"agiloft_attachment_info","name":"Agiloft Attachment Info","description":"Get information about file attachments on a record field.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to check attachments on"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field to inspect"}},"hostedApiKey":"none"},"agiloft_create_record":{"id":"agiloft_create_record","name":"Agiloft Create Record","description":"Create a new record in an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record field values as a JSON object (e.g., {\\"first_name\\": \\"John\\", \\"status\\": \\"Active\\"})"}},"hostedApiKey":"none"},"agiloft_delete_record":{"id":"agiloft_delete_record","name":"Agiloft Delete Record","description":"Delete a record from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to delete"},"substituteIds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated IDs of records that adopt the dependants of the deleted record. Read only when the delete rule is REPLACE_WITH_ANOTHER."},"deleteRule":{"type":"string","required":false,"visibility":"user-or-llm","description":"How to treat records that depend on this one: ERROR_IF_DEPENDANTS (default — fails rather than cascading), APPLY_DELETE_WHERE_POSSIBLE, DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK, APPLY_UNLINK, UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE, or REPLACE_WITH_ANOTHER"}},"hostedApiKey":"none"},"agiloft_get_choice_line_id":{"id":"agiloft_get_choice_line_id","name":"Agiloft Get Choice Line ID","description":"Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"case\\", \\"contracts\\")"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Choice field name (e.g., \\"priority\\", \\"status\\")"},"value":{"type":"string","required":true,"visibility":"user-or-llm","description":"Choice display value to resolve (e.g., \\"High\\", \\"Active\\")"}},"hostedApiKey":"none"},"agiloft_list_tables":{"id":"agiloft_list_tables","name":"Agiloft List Tables","description":"List the tables and fields in an Agiloft knowledge base, to discover the logical names other operations need.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":false,"visibility":"user-or-llm","description":"Logical name of a single table to describe (e.g., \\"contacts\\"). Leave empty to list every table in the knowledge base."},"includeLinkedInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the source table and column behind each linked field"},"skipColumnsInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Return table names only, omitting field details, for a much smaller response"}},"hostedApiKey":"none"},"agiloft_lock_record":{"id":"agiloft_lock_record","name":"Agiloft Lock Record","description":"Lock, unlock, or check the lock status of an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to lock, unlock, or check"},"lockAction":{"type":"string","required":true,"visibility":"user-or-llm","description":"Action to perform: \\"lock\\", \\"unlock\\", or \\"check\\""},"force":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Unlock only: release a lock held by another user."}},"hostedApiKey":"none"},"agiloft_nlp_search":{"id":"agiloft_nlp_search","name":"Agiloft Natural Language Search","description":"Search Agiloft records by describing what you want in plain language, such as \\"active NDAs submitted last month\\".","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"nlpQuery":{"type":"string","required":true,"visibility":"user-or-llm","description":"The request in plain language, e.g. \\"Show me open, high-priority contracts\\". Structured field filters are not accepted — use Search Records for those."},"fields":{"type":"string","required":true,"visibility":"user-or-llm","description":"Comma-separated field names to return, e.g. \\"id, contract_title1, company_name\\""},"page":{"type":"string","required":false,"visibility":"user-or-llm","description":"Page number, starting from 0"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Records per page"}},"hostedApiKey":"none"},"agiloft_read_record":{"id":"agiloft_read_record","name":"Agiloft Read Record","description":"Read a record by ID from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to read"},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of field names to include in the response"}},"hostedApiKey":"none"},"agiloft_remove_attachment":{"id":"agiloft_remove_attachment","name":"Agiloft Remove Attachment","description":"Remove an attached file from a field in an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record containing the attachment"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"position":{"type":"string","required":true,"visibility":"user-or-llm","description":"Position index of the file to remove (starting from 0)"}},"hostedApiKey":"none"},"agiloft_retrieve_attachment":{"id":"agiloft_retrieve_attachment","name":"Agiloft Retrieve Attachment","description":"Download an attached file from an Agiloft record field.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record containing the attachment"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"position":{"type":"string","required":true,"visibility":"user-or-llm","description":"Position index of the file in the field (starting from 0)"}},"hostedApiKey":"none"},"agiloft_run_action_button":{"id":"agiloft_run_action_button","name":"Agiloft Run Action Button","description":"Run an action button on an Agiloft record, such as an approval or send-for-signature step.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"case\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to run the action button on"},"actionButtonField":{"type":"string","required":true,"visibility":"user-or-llm","description":"Logical name of the field holding the action button (e.g., \\"ab_field\\")"}},"hostedApiKey":"none"},"agiloft_saved_search":{"id":"agiloft_saved_search","name":"Agiloft Saved Search","description":"List the saved searches defined for an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Logical table name to list saved searches for (e.g., \\"contract\\")"}},"hostedApiKey":"none"},"agiloft_search_records":{"id":"agiloft_search_records","name":"Agiloft Search Records","description":"Search for records in an Agiloft table using a query.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name to search in (e.g., \\"contracts\\", \\"contacts.employees\\")"},"query":{"type":"string","required":false,"visibility":"user-or-llm","description":"Ad hoc EWSearch query. Combine conditions with && (and) or || (or) and quote every value — e.g. \\"summary~=\'test\'&&priority=\'High\'\\". Required unless a saved search is given."},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Label of a saved search defined on the table (e.g., \\"C: Status is Closed\\"). Can be combined with a query to narrow it further."},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of field names to include in the results"},"page":{"type":"string","required":false,"visibility":"user-or-llm","description":"Page number for paginated results (starting from 0)"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of records to return per page. Agiloft treats 0 as \\"all records\\", so leave it unset or use a positive value to keep result sizes bounded."}},"hostedApiKey":"none"},"agiloft_select_records":{"id":"agiloft_select_records","name":"Agiloft Select Records","description":"Select record IDs matching a SQL WHERE clause from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"where":{"type":"string","required":true,"visibility":"user-or-llm","description":"SQL WHERE clause using database column names (e.g., \\"summary like \'%new%\'\\" or \\"assigned_person=\'John Doe\'\\"). EWSelect has no page size and returns every matching ID, so append a database limit such as \\"limit 0,200\\" to bound the result."}},"hostedApiKey":"none"},"agiloft_update_record":{"id":"agiloft_update_record","name":"Agiloft Update Record","description":"Update an existing record in an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to update"},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Updated field values as a JSON object (e.g., {\\"status\\": \\"Active\\", \\"priority\\": \\"High\\"})"}},"hostedApiKey":"none"},"agiloft_upsert_record":{"id":"agiloft_upsert_record","name":"Agiloft Upsert Record","description":"Create an Agiloft record, or update it when a record already matches the given fields.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"match":{"type":"string","required":true,"visibility":"user-or-llm","description":"Field used to find an existing record (e.g., \\"ext_id\\"). Pick something that identifies a record uniquely — if more than one record matches, Agiloft writes nothing and returns a conflict."},"async":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Queue the write instead of waiting for it. Returns a callback ID instead of a record ID; pass that to Async Status to poll the result."},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Field values as a JSON object. On create these populate the new record; on update only the supplied fields change."}},"hostedApiKey":"none"},"ahrefs_anchors":{"id":"ahrefs_anchors","name":"Ahrefs Anchors","description":"Get the anchor text distribution for a target domain or URL\'s backlinks, showing how many links and referring domains use each anchor text.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live), \\"all_time\\" (default, includes lost backlinks), or \\"since:YYYY-MM-DD\\" (backlinks found since a date)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_backlinks":{"id":"ahrefs_backlinks","name":"Ahrefs Backlinks","description":"Get a list of backlinks pointing to a target domain or URL. Returns details about each backlink including source URL, anchor text, and domain rating.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live backlinks), \\"all_time\\" (default, includes lost backlinks), or \\"since:YYYY-MM-DD\\" (backlinks found since a date)."},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_backlinks_stats":{"id":"ahrefs_backlinks_stats","name":"Ahrefs Backlinks Stats","description":"Get backlink and referring domain totals for a target domain or URL, both currently live and across all time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_batch_analysis":{"id":"ahrefs_batch_analysis","name":"Ahrefs Batch Analysis","description":"Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once.","version":"1.0.0","params":{"targets":{"type":"string","required":true,"visibility":"user-or-llm","description":"Comma-separated list of domains or URLs to analyze. Example: \\"example.com,competitor.com\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode applied to every target: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"protocol":{"type":"string","required":false,"visibility":"user-or-llm","description":"Protocol applied to every target: \\"both\\" (default), \\"http\\", or \\"https\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_broken_backlinks":{"id":"ahrefs_broken_backlinks","name":"Ahrefs Broken Backlinks","description":"Get a list of broken backlinks pointing to a target domain or URL. Useful for identifying link reclamation opportunities.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_domain_rating":{"id":"ahrefs_domain_rating","name":"Ahrefs Domain Rating","description":"Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website\'s backlink profile on a scale from 0 to 100.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain to analyze (e.g., example.com)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date for historical data in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_domain_rating_history":{"id":"ahrefs_domain_rating_history","name":"Ahrefs Domain Rating History","description":"Get the historical Domain Rating (DR) trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_keyword_overview":{"id":"ahrefs_keyword_overview","name":"Ahrefs Keyword Overview","description":"Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential.","version":"1.0.0","params":{"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The keyword to analyze"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for keyword data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_keywords_history":{"id":"ahrefs_keywords_history","name":"Ahrefs Keywords History","description":"Get the historical organic keyword ranking distribution for a target domain or URL over a date range: how many keywords rank in each position bucket at each point in time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_metrics":{"id":"ahrefs_metrics","name":"Ahrefs Metrics","description":"Get a one-call organic and paid search overview for a target domain or URL: organic traffic, organic keywords, paid traffic, paid keywords, and estimated traffic cost.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_metrics_history":{"id":"ahrefs_metrics_history","name":"Ahrefs Metrics History","description":"Get the historical organic and paid traffic trend for a target domain or URL over a date range: organic traffic/cost and paid traffic/cost at each point in time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_organic_competitors":{"id":"ahrefs_organic_competitors","name":"Ahrefs Organic Competitors","description":"Get domains that compete with a target domain or URL for the same organic keywords, ranked by keyword overlap.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_organic_keywords":{"id":"ahrefs_organic_keywords","name":"Ahrefs Organic Keywords","description":"Get organic keywords that a target domain or URL ranks for in Google search results. Returns keyword details including search volume, ranking position, and estimated traffic.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_paid_pages":{"id":"ahrefs_paid_pages","name":"Ahrefs Paid Pages","description":"Get a target domain\'s pages that receive paid search traffic, sorted by estimated paid traffic. Returns page URLs with their paid traffic, keyword counts, and estimated spend.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_rank_tracker_competitors_overview":{"id":"ahrefs_rank_tracker_competitors_overview","name":"Ahrefs Rank Tracker Competitors Overview","description":"Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker project: each tracked keyword\'s volume and difficulty alongside every competitor\'s position, traffic, and traffic value. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report rankings for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"dateCompared":{"type":"string","required":false,"visibility":"user-only","description":"Comparison date in YYYY-MM-DD format, to compute position/traffic deltas"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_rank_tracker_competitors_stats":{"id":"ahrefs_rank_tracker_competitors_stats","name":"Ahrefs Rank Tracker Competitors Stats","description":"Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competitor\'s traffic, traffic value, average position, and share of voice across all tracked keywords. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report metrics for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_rank_tracker_overview":{"id":"ahrefs_rank_tracker_overview","name":"Ahrefs Rank Tracker Overview","description":"Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker project: position, search volume, keyword difficulty, and estimated traffic. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report rankings for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"dateCompared":{"type":"string","required":false,"visibility":"user-only","description":"Comparison date in YYYY-MM-DD format, to compute position/traffic deltas"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_rank_tracker_serp_overview":{"id":"ahrefs_rank_tracker_serp_overview","name":"Ahrefs Rank Tracker SERP Overview","description":"Get the full SERP (search engine results page) for a keyword tracked in an Ahrefs Rank Tracker project, including every ranking URL with its position, title, and authority metrics. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The tracked keyword to retrieve SERP data for"},"country":{"type":"string","required":true,"visibility":"user-or-llm","description":"Country code for the tracked keyword. Example: \\"us\\", \\"gb\\", \\"de\\""},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"topPositions":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of top organic positions to return (defaults to all available)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Timestamp to return the last available SERP Overview at, in YYYY-MM-DDThh:mm:ss format"},"locationId":{"type":"number","required":false,"visibility":"user-or-llm","description":"Location ID of the tracked keyword, if tracked at a specific location"},"languageCode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Language code of the tracked keyword"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_refdomains_history":{"id":"ahrefs_refdomains_history","name":"Ahrefs Referring Domains History","description":"Get the historical referring domains trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_referring_domains":{"id":"ahrefs_referring_domains","name":"Ahrefs Referring Domains","description":"Get a list of domains that link to a target domain or URL. Returns unique referring domains with their domain rating, backlink counts, and discovery dates.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live), \\"all_time\\" (default, includes lost domains), or \\"since:YYYY-MM-DD\\" (domains found since a date)."},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_related_terms":{"id":"ahrefs_related_terms","name":"Ahrefs Related Terms","description":"Get keyword ideas related to a seed keyword: terms the same top-ranking pages also rank for (\\"also rank for\\") or also discuss (\\"also talk about\\"), with volume, difficulty, and CPC.","version":"1.0.0","params":{"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The seed keyword to find related terms for"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for keyword data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"terms":{"type":"string","required":false,"visibility":"user-or-llm","description":"Type of related keywords to return: \\"also_rank_for\\", \\"also_talk_about\\", or \\"all\\" (default: \\"all\\")"},"viewFor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Whether to derive related terms from the top 10 or top 100 ranking pages (default: \\"top_10\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_site_audit_page_explorer":{"id":"ahrefs_site_audit_page_explorer","name":"Ahrefs Site Audit Page Explorer","description":"Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: HTTP status, title, link counts, backlinks, indexability, and traffic. Optionally filter to pages affected by a specific issue.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Site Audit project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Crawl date in YYYY-MM-DDThh:mm:ss format (defaults to the most recent crawl)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip, for pagination"},"issueId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Only return pages affected by this issue ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"ahrefs_top_pages":{"id":"ahrefs_top_pages","name":"Ahrefs Top Pages","description":"Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}},"hostedApiKey":"none"},"airtable_create_records":{"id":"airtable_create_records","name":"Airtable Create Records","description":"Write new records to an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to create, each with a `fields` object"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_delete_records":{"id":"airtable_delete_records","name":"Airtable Delete Records","description":"Delete one or more records from an Airtable table by ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordIds":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of record IDs to delete (each starts with \\"rec\\", e.g., [\\"recXXXXXXXXXXXXXX\\"]). Pass a single-element array to delete one record."}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_get_base_schema":{"id":"airtable_get_base_schema","name":"Airtable Get Base Schema","description":"Get the schema of all tables, fields, and views in an Airtable base","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_get_record":{"id":"airtable_get_record","name":"Airtable Get Record","description":"Retrieve a single record from an Airtable table by its ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record ID to retrieve (starts with \\"rec\\", e.g., \\"recXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_list_bases":{"id":"airtable_list_bases","name":"Airtable List Bases","description":"List all bases the authenticated user has access to","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"offset":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination offset for retrieving additional bases"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_list_records":{"id":"airtable_list_records","name":"Airtable List Records","description":"Read records from an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"maxRecords":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of records to return (default: all records)"},"filterFormula":{"type":"string","required":false,"visibility":"user-or-llm","description":"Formula to filter records (e.g., \\"({Field Name} = \'Value\')\\")"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_list_tables":{"id":"airtable_list_tables","name":"Airtable List Tables","description":"List all tables and their schema in an Airtable base","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_update_multiple_records":{"id":"airtable_update_multiple_records","name":"Airtable Update Multiple Records","description":"Update multiple existing records in an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to update, each with an `id` and a `fields` object"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_update_record":{"id":"airtable_update_record","name":"Airtable Update Record","description":"Update an existing record in an Airtable table by ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record ID to update (starts with \\"rec\\", e.g., \\"recXXXXXXXXXXXXXX\\")"},"fields":{"type":"json","required":true,"visibility":"user-or-llm","description":"An object containing the field names and their new values"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airtable_upsert_records":{"id":"airtable_upsert_records","name":"Airtable Upsert Records","description":"Update existing records or create new ones in an Airtable table, matching on the specified merge fields","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to upsert, each with a `fields` object"},"fieldsToMergeOn":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of field names used to match existing records (max 3). A record is updated when all merge fields match, otherwise it is created. Example: [\\"Name\\"]"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"},"hostedApiKey":"none"},"airweave_search":{"id":"airweave_search","name":"Airweave Search","description":"Search your synced data collections using Airweave. Supports semantic search with hybrid, neural, or keyword retrieval strategies. Optionally generate AI-powered answers from search results.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Airweave API Key for authentication"},"collectionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The readable ID of the collection to search"},"query":{"type":"string","required":true,"visibility":"user-or-llm","description":"The search query text"},"limit":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 100)"},"retrievalStrategy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Retrieval strategy: hybrid (default), neural, or keyword"},"expandQuery":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Generate query variations to improve recall"},"rerank":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Reorder results for improved relevance using LLM"},"generateAnswer":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Generate a natural-language answer to the query"}},"hostedApiKey":"none"},"algolia_add_record":{"id":"algolia_add_record","name":"Algolia Add Record","description":"Add or replace a record in an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":false,"visibility":"user-or-llm","description":"Object ID for the record (auto-generated if not provided)"},"record":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object representing the record to add"}},"hostedApiKey":"none"},"algolia_batch_operations":{"id":"algolia_batch_operations","name":"Algolia Batch Operations","description":"Perform batch add, update, partial update, or delete operations on records in an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"requests":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of batch operations. Each item has \\"action\\" (addObject, updateObject, partialUpdateObject, partialUpdateObjectNoCreate, deleteObject, delete, clear) and \\"body\\" (the record data; must include objectID for update/delete; use an empty object {} for the index-level delete/clear actions)"}},"hostedApiKey":"none"},"algolia_browse_records":{"id":"algolia_browse_records","name":"Algolia Browse Records","description":"Browse and iterate over all records in an Algolia index using cursor pagination","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key (must have browse ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to browse"},"query":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search query to filter browsed records"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter string to narrow down results"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of hits per page (default: 1000, max: 1000)"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous browse response for pagination"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search"}},"hostedApiKey":"none"},"algolia_clear_records":{"id":"algolia_clear_records","name":"Algolia Clear Records","description":"Clear all records from an Algolia index while keeping settings, synonyms, and rules","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to clear"}},"hostedApiKey":"none"},"algolia_copy_move_index":{"id":"algolia_copy_move_index","name":"Algolia Copy/Move Index","description":"Copy or move an Algolia index to a new destination","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the source index"},"operation":{"type":"string","required":true,"visibility":"user-or-llm","description":"Operation to perform: \\"copy\\" or \\"move\\""},"destination":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the destination index"},"scope":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of scopes to copy (only for \\"copy\\" operation): [\\"settings\\", \\"synonyms\\", \\"rules\\"]. Omit to copy everything including records."}},"hostedApiKey":"none"},"algolia_delete_by_filter":{"id":"algolia_delete_by_filter","name":"Algolia Delete By Filter","description":"Delete all records matching a filter from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter expression to match records for deletion (e.g., \\"category:outdated\\")"},"facetFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of facet filters (e.g., [\\"brand:Acme\\"])"},"numericFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of numeric filters (e.g., [\\"price > 100\\"])"},"tagFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of tag filters using the _tags attribute (e.g., [\\"published\\"])"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search filter (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search filter"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search filter"}},"hostedApiKey":"none"},"algolia_delete_index":{"id":"algolia_delete_index","name":"Algolia Delete Index","description":"Delete an entire Algolia index and all its records","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to delete"}},"hostedApiKey":"none"},"algolia_delete_record":{"id":"algolia_delete_record","name":"Algolia Delete Record","description":"Delete a record by objectID from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to delete"}},"hostedApiKey":"none"},"algolia_get_record":{"id":"algolia_get_record","name":"Algolia Get Record","description":"Get a record by objectID from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to retrieve"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"}},"hostedApiKey":"none"},"algolia_get_records":{"id":"algolia_get_records","name":"Algolia Get Records","description":"Retrieve multiple records by objectID from one or more Algolia indices","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Default index name for all requests"},"requests":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of objects specifying records to retrieve. Each must have \\"objectID\\" and optionally \\"indexName\\" and \\"attributesToRetrieve\\"."}},"hostedApiKey":"none"},"algolia_get_settings":{"id":"algolia_get_settings","name":"Algolia Get Settings","description":"Retrieve the settings of an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"}},"hostedApiKey":"none"},"algolia_get_task_status":{"id":"algolia_get_task_status","name":"Algolia Get Task Status","description":"Check whether an Algolia indexing task has finished publishing","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index the task ran against"},"taskID":{"type":"number","required":true,"visibility":"user-or-llm","description":"The taskID returned by a previous write operation"}},"hostedApiKey":"none"},"algolia_list_indices":{"id":"algolia_list_indices","name":"Algolia List Indices","description":"List all indices in an Algolia application","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for paginating indices (default: not paginated)"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of indices per page (default: 100)"}},"hostedApiKey":"none"},"algolia_partial_update_record":{"id":"algolia_partial_update_record","name":"Algolia Partial Update Record","description":"Partially update a record in an Algolia index without replacing it entirely","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to update"},"attributes":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object with attributes to update. Supports built-in operations like {\\"stock\\": {\\"_operation\\": \\"Decrement\\", \\"value\\": 1}}"},"createIfNotExists":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to create the record if it does not exist (default: true)"}},"hostedApiKey":"none"},"algolia_search":{"id":"algolia_search","name":"Algolia Search","description":"Search an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to search"},"query":{"type":"string","required":true,"visibility":"user-or-llm","description":"Search query text"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of hits per page (default: 20)"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number to retrieve (default: 0)"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter string (e.g., \\"category:electronics AND price < 100\\")"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"},"facets":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of facet attribute names to retrieve counts for (use \\"*\\" for all)"},"getRankingInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to include detailed ranking information in each hit"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search"}},"hostedApiKey":"none"},"algolia_update_settings":{"id":"algolia_update_settings","name":"Algolia Update Settings","description":"Update the settings of an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have editSettings ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"settings":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object with settings to update (e.g., {\\"searchableAttributes\\": [\\"name\\", \\"description\\"], \\"customRanking\\": [\\"desc(popularity)\\"]})"},"forwardToReplicas":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to apply changes to replica indices (default: false)"}},"hostedApiKey":"none"},"amplitude_event_segmentation":{"id":"amplitude_event_segmentation","name":"Amplitude Event Segmentation","description":"Query event analytics data with segmentation. Get event counts, uniques, averages, and more.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"eventType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Event type name to analyze"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric type: uniques, totals, pct_dau, average, histogram, sums, value_avg, or formula (default: uniques)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by (prefix custom user properties with \\"gp:\\")"},"groupBy2":{"type":"string","required":false,"visibility":"user-or-llm","description":"Second property name to group by (prefix custom user properties with \\"gp:\\")"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of group-by values (max 1000)"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON array of filter objects applied to the event, e.g. [{\\"subprop_type\\":\\"event\\",\\"subprop_key\\":\\"city\\",\\"subprop_op\\":\\"is\\",\\"subprop_value\\":[\\"San Francisco\\"]}]"},"formula":{"type":"string","required":false,"visibility":"user-or-llm","description":"Required when metric is \\"formula\\", e.g. \\"UNIQUES(A)/UNIQUES(B)\\""},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_funnels":{"id":"amplitude_funnels","name":"Amplitude Funnels","description":"Analyze conversion rates and drop-off between a sequence of events.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"events":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON array of event objects, one per funnel step in order, e.g. [{\\"event_type\\":\\"signup\\"},{\\"event_type\\":\\"purchase\\"}]"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Funnel ordering: \\"ordered\\", \\"unordered\\", or \\"sequential\\" (default: ordered)"},"userType":{"type":"string","required":false,"visibility":"user-or-llm","description":"User type: \\"new\\" or \\"active\\" (default: active)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: -300000 (real-time), -3600000 (hourly), 1 (daily), 7 (weekly), or 30 (monthly)"},"conversionWindowSeconds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Conversion window in seconds (default: 2592000, i.e. 30 days)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property to group by (limit: one; prefix custom properties with \\"gp:\\")"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of group-by values (default: 100, max: 1000)"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_get_active_users":{"id":"amplitude_get_active_users","name":"Amplitude Get Active Users","description":"Get active or new user counts over a date range from the Dashboard REST API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric type: \\"active\\" or \\"new\\" (default: active)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_get_revenue":{"id":"amplitude_get_revenue","name":"Amplitude Get Revenue","description":"Get revenue LTV data including ARPU, ARPPU, total revenue, and paying user counts.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric: 0 (ARPU), 1 (ARPPU), 2 (Total Revenue), 3 (Paying Users)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by (limit: one)"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_group_identify":{"id":"amplitude_group_identify","name":"Amplitude Group Identify","description":"Set group-level properties in Amplitude. Supports $set, $setOnce, $add, $append, $unset operations.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"groupType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Group classification (e.g., \\"company\\", \\"org_id\\")"},"groupValue":{"type":"string","required":true,"visibility":"user-or-llm","description":"Specific group identifier (e.g., \\"Acme Corp\\")"},"groupProperties":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON object of group properties. Use operations like $set, $setOnce, $add, $append, $unset."},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_identify_user":{"id":"amplitude_identify_user","name":"Amplitude Identify User","description":"Set user properties in Amplitude using the Identify API. Supports $set, $setOnce, $add, $append, $unset operations.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"User ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"userProperties":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON object of user properties. Use operations like $set, $setOnce, $add, $append, $unset."},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_list_events":{"id":"amplitude_list_events","name":"Amplitude List Events","description":"List all event types in the Amplitude project with their weekly totals and unique counts.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_realtime_active_users":{"id":"amplitude_realtime_active_users","name":"Amplitude Real-time Active Users","description":"Get real-time active user counts at 5-minute granularity for the last 2 days.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_retention":{"id":"amplitude_retention","name":"Amplitude Retention","description":"Measure how many users return to perform an action after a starting action.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"startEvent":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON starting event object, e.g. {\\"event_type\\":\\"_new\\"} or {\\"event_type\\":\\"_active\\"}"},"returnEvent":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON returning event object, e.g. {\\"event_type\\":\\"_all\\"} or {\\"event_type\\":\\"_active\\"}"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"retentionMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Retention type: \\"bracket\\", \\"rolling\\", or \\"n-day\\" (default: n-day)"},"retentionBrackets":{"type":"string","required":false,"visibility":"user-or-llm","description":"Required when Retention Mode is \\"bracket\\". Day ranges, e.g. [[0,4]]"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property to group by (limit: one; prefix custom properties with \\"gp:\\")"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_send_event":{"id":"amplitude_send_event","name":"Amplitude Send Event","description":"Track an event in Amplitude using the HTTP V2 API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"User ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"eventType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the event (e.g., \\"page_view\\", \\"purchase\\")"},"eventProperties":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON object of custom event properties"},"userProperties":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON object of user properties to set (supports $set, $setOnce, $add, $append, $unset)"},"time":{"type":"string","required":false,"visibility":"user-or-llm","description":"Event timestamp in milliseconds since epoch"},"sessionId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Session start time in milliseconds since epoch"},"insertId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Unique ID for deduplication (within 7-day window)"},"appVersion":{"type":"string","required":false,"visibility":"user-or-llm","description":"Application version string"},"platform":{"type":"string","required":false,"visibility":"user-or-llm","description":"Platform (e.g., \\"Web\\", \\"iOS\\", \\"Android\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Two-letter country code"},"language":{"type":"string","required":false,"visibility":"user-or-llm","description":"Language code (e.g., \\"en\\")"},"ip":{"type":"string","required":false,"visibility":"user-or-llm","description":"IP address for geo-location"},"price":{"type":"string","required":false,"visibility":"user-or-llm","description":"Price of the item purchased"},"quantity":{"type":"string","required":false,"visibility":"user-or-llm","description":"Quantity of items purchased"},"revenue":{"type":"string","required":false,"visibility":"user-or-llm","description":"Revenue amount"},"productId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Product identifier"},"revenueType":{"type":"string","required":false,"visibility":"user-or-llm","description":"Revenue type (e.g., \\"purchase\\", \\"refund\\")"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_user_activity":{"id":"amplitude_user_activity","name":"Amplitude User Activity","description":"Get the event stream for a specific user by their Amplitude ID.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"amplitudeId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Amplitude internal user ID"},"offset":{"type":"string","required":false,"visibility":"user-or-llm","description":"Offset for pagination (default 0)"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of events to return (default 1000, max 1000)"},"direction":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort direction: \\"latest\\" or \\"earliest\\" (default: latest)"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"amplitude_user_profile":{"id":"amplitude_user_profile","name":"Amplitude User Profile","description":"Get a user profile including properties, cohort memberships, and computed properties. Not available for EU data-residency projects.","version":"1.0.0","params":{"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"External user ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"getAmpProps":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include Amplitude user properties (true/false, default: false)"},"getCohortIds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include cohort IDs the user belongs to (true/false, default: false)"},"getComputations":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include computed user properties (true/false, default: false)"}},"hostedApiKey":"none"},"amplitude_user_search":{"id":"amplitude_user_search","name":"Amplitude User Search","description":"Search for a user by User ID, Device ID, or Amplitude ID using the Dashboard REST API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"user":{"type":"string","required":true,"visibility":"user-or-llm","description":"User ID, Device ID, or Amplitude ID to search for"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}},"hostedApiKey":"none"},"apify_get_dataset_items":{"id":"apify_get_dataset_items","name":"APIFY Get Dataset Items","description":"Retrieve items stored in an APIFY dataset","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"datasetId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Dataset ID to read items from. Example: \\"9RnD3Pql2vGZkc5H5\\""},"itemLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Max items to return (1-250000). Default: all items. Example: 500"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to skip at the start. Default: 0"},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of fields to include. Example: \\"title,url,price\\""}},"hostedApiKey":"none"},"apify_get_run":{"id":"apify_get_run","name":"APIFY Get Run","description":"Get the status and details of an APIFY actor run","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"runId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor run ID to fetch. Example: \\"HG7ML7M8z78YcAPEB\\""}},"hostedApiKey":"none"},"apify_run_actor_async":{"id":"apify_run_actor_async","name":"APIFY Run Actor (Async)","description":"Run an APIFY actor asynchronously with polling for long-running tasks","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"actorId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor ID or username/actor-name. Examples: \\"apify/web-scraper\\", \\"janedoe/my-actor\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor input as JSON string. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}], \\"maxPages\\": 10}"},"waitForFinish":{"type":"number","required":false,"visibility":"user-or-llm","description":"Initial wait time in seconds (0-60) before polling starts. Example: 30"},"itemLimit":{"type":"number","required":false,"default":100,"visibility":"user-or-llm","description":"Max dataset items to fetch (1-250000). Default: 100. Example: 500"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the actor run (128-32768). Example: 1024 for 1GB, 2048 for 2GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\", \\"build-tag-name\\""}},"hostedApiKey":"none"},"apify_run_actor_sync":{"id":"apify_run_actor_sync","name":"APIFY Run Actor (Sync)","description":"Run an APIFY actor synchronously and get results (max 5 minutes)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"actorId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor ID or username/actor-name. Examples: \\"apify/web-scraper\\", \\"janedoe/my-actor\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor input as JSON string. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}], \\"maxPages\\": 10}"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the actor run (128-32768). Example: 1024 for 1GB, 2048 for 2GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\", \\"build-tag-name\\""}},"hostedApiKey":"none"},"apify_run_task":{"id":"apify_run_task","name":"APIFY Run Task","description":"Run a saved APIFY actor task synchronously and get dataset items (max 5 minutes)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task ID or username/task-name. Examples: \\"janedoe/my-task\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON string that overrides the task\'s saved input. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}]}"},"itemLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Max dataset items to return (1-250000). Example: 500"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the run (128-32768). Example: 1024 for 1GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the run. Example: 300 for 5 minutes"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\""}},"hostedApiKey":"none"},"apollo_account_bulk_create":{"id":"apollo_account_bulk_create","name":"Apollo Bulk Create Accounts","description":"Create up to 100 accounts at once in your Apollo database. Set run_dedupe=true to deduplicate by domain, organization_id, and name. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"accounts":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of accounts to create (max 100). Each account should include a name, and may optionally include domain, phone, phone_status_cd, raw_address, owner_id, linkedin_url, facebook_url, twitter_url, salesforce_id, and hubspot_id."},"append_label_names":{"type":"array","required":false,"visibility":"user-only","description":"Array of label names to add to ALL accounts in this request"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, performs aggressive deduplication by domain, organization_id, and name (defaults to false)"}},"hostedApiKey":"none"},"apollo_account_bulk_update":{"id":"apollo_account_bulk_update","name":"Apollo Bulk Update Accounts","description":"Update up to 1000 existing accounts at once in your Apollo database (higher limit than contacts!). Each account must include an id field. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"account_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of account IDs to update with the same values (max 1000). Use with name/owner_id for uniform updates. Use either this OR account_attributes."},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this name to all accounts"},"owner_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this owner to all accounts"},"account_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this account stage to all accounts"},"account_attributes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of account objects with individual updates (each must include id). Example: [{\\"id\\": \\"acc1\\", \\"name\\": \\"Acme\\", \\"owner_id\\": \\"u1\\", \\"account_stage_id\\": \\"s1\\", \\"typed_custom_fields\\": {\\"field_id\\": \\"value\\"}}]"},"async":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, processes the update asynchronously. Only supported when using account_ids; returns 422 if used with account_attributes."}},"hostedApiKey":"none"},"apollo_account_create":{"id":"apollo_account_create","name":"Apollo Create Account","description":"Create a new account (company) in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Company name (e.g., \\"Acme Corporation\\")"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain without www. prefix (e.g., \\"acme.com\\")"},"phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number for the account"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo user ID of the account owner"},"account_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo ID for the account stage to assign this account to"},"raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate location (e.g., \\"San Francisco, CA, USA\\")"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}},"hostedApiKey":"none"},"apollo_account_search":{"id":"apollo_account_search","name":"Apollo Search Accounts","description":"Search your team\'s accounts in Apollo. Display limit: 50,000 records (100 records per page, 500 pages max). Use filters to narrow results. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"q_organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter accounts by organization name (partial-match search)"},"account_stage_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by account stage IDs"},"account_label_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by account label IDs"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"account_last_activity_date\\", \\"account_created_at\\", or \\"account_updated_at\\""},"sort_ascending":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Sort ascending when true. Defaults to descending."},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_account_update":{"id":"apollo_account_update","name":"Apollo Update Account","description":"Update an existing account in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"account_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the account to update (e.g., \\"acc_abc123\\")"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company name (e.g., \\"Acme Corporation\\")"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain (e.g., \\"acme.com\\")"},"phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company phone number"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo user ID of the account owner"},"account_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo ID for the account stage to assign this account to"},"raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate location (e.g., \\"San Francisco, CA, USA\\")"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}},"hostedApiKey":"none"},"apollo_contact_bulk_create":{"id":"apollo_contact_bulk_create","name":"Apollo Bulk Create Contacts","description":"Create up to 100 contacts at once in your Apollo database. Supports deduplication to prevent creating duplicate contacts. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"contacts":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of contacts to create (max 100). Each contact may include first_name, last_name, email, title, organization_name, account_id, owner_id, contact_stage_id, linkedin_url, phone (single string) or phone_numbers (array of {raw_number, position}), contact_emails, typed_custom_fields, and CRM IDs (salesforce_contact_id, hubspot_id, team_id) for cross-system matching"},"append_label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Label names to add to all contacts in this request (e.g., [\\"Hot Lead\\"])"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-only","description":"Enable deduplication to prevent creating duplicate contacts. When true, existing contacts are returned without modification"}},"hostedApiKey":"none"},"apollo_contact_bulk_update":{"id":"apollo_contact_bulk_update","name":"Apollo Bulk Update Contacts","description":"Update up to 100 existing contacts at once in your Apollo database. Each contact must include an id field. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"contact_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of contact IDs to update. Must be paired with an object-form contact_attributes specifying the fields to apply uniformly to all listed contacts."},"contact_attributes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Required. Either an array of per-contact updates (each with id) — used standalone — or a single object of attributes to apply to all contact_ids. Supported fields: owner_id, email, organization_name, title, first_name, last_name, account_id, present_raw_address, linkedin_url, typed_custom_fields"},"async":{"type":"boolean","required":false,"visibility":"user-only","description":"Force asynchronous processing. Automatically enabled for >100 contacts"}},"hostedApiKey":"none"},"apollo_contact_create":{"id":"apollo_contact_create","name":"Apollo Create Contact","description":"Create a new contact in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"first_name":{"type":"string","required":true,"visibility":"user-or-llm","description":"First name of the contact"},"last_name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Last name of the contact"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address of the contact"},"title":{"type":"string","required":false,"visibility":"user-or-llm","description":"Job title (e.g., \\"VP of Sales\\", \\"Software Engineer\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo account ID to associate with (e.g., \\"acc_abc123\\")"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the contact owner (accepted by Apollo but not officially documented for POST /contacts)"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the contact\'s employer (e.g., \\"Apollo\\")"},"website_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate website URL (e.g., \\"https://www.apollo.io/\\")"},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Lists/labels to add the contact to (e.g., [\\"Prospects\\"])"},"contact_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the contact stage"},"present_raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal location for the contact (e.g., \\"Atlanta, United States\\")"},"direct_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number"},"corporate_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Work/office phone number"},"mobile_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Mobile phone number"},"home_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Home phone number"},"other_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Alternative phone number"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom field values keyed by custom field ID"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, Apollo deduplicates against existing contacts"}},"hostedApiKey":"none"},"apollo_contact_search":{"id":"apollo_contact_search","name":"Apollo Search Contacts","description":"Search your team\'s contacts in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"q_keywords":{"type":"string","required":false,"visibility":"user-or-llm","description":"Keywords to search for"},"contact_stage_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by contact stage IDs"},"contact_label_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by Apollo label IDs (lists)"},"sort_by_field":{"type":"string","required":false,"visibility":"user-only","description":"Sort field: contact_last_activity_date, contact_email_last_opened_at, contact_email_last_clicked_at, contact_created_at, or contact_updated_at"},"sort_ascending":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, sort ascending. Must be used together with sort_by_field"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_contact_update":{"id":"apollo_contact_update","name":"Apollo Update Contact","description":"Update an existing contact in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"contact_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the contact to update (e.g., \\"con_abc123\\")"},"first_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"First name of the contact"},"last_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Last name of the contact"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address"},"title":{"type":"string","required":false,"visibility":"user-or-llm","description":"Job title (e.g., \\"VP of Sales\\", \\"Software Engineer\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo account ID (e.g., \\"acc_abc123\\")"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the contact owner (accepted by Apollo but not officially documented for PATCH /contacts/{id})"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the contact\'s employer (e.g., \\"Apollo\\")"},"website_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate website URL (e.g., \\"https://www.apollo.io/\\")"},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Lists/labels to add the contact to (e.g., [\\"Prospects\\"])"},"contact_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the contact stage"},"present_raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal location for the contact (e.g., \\"Atlanta, United States\\")"},"direct_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number"},"corporate_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Work/office phone number"},"mobile_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Mobile phone number"},"home_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Home phone number"},"other_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Alternative phone number"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom field values keyed by custom field ID"}},"hostedApiKey":"none"},"apollo_email_accounts":{"id":"apollo_email_accounts","name":"Apollo Get Email Accounts","description":"Get list of team\'s linked email accounts in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"}},"hostedApiKey":"none"},"apollo_opportunity_create":{"id":"apollo_opportunity_create","name":"Apollo Create Opportunity","description":"Create a new deal for an account in your Apollo database (master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the opportunity/deal (e.g., \\"Enterprise License - Q1\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"ID of the account this opportunity belongs to (e.g., \\"acc_abc123\\")"},"amount":{"type":"string","required":false,"visibility":"user-or-llm","description":"Monetary value as a plain number string with no commas or currency symbols"},"opportunity_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the opportunity stage"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the opportunity owner"},"closed_date":{"type":"string","required":false,"visibility":"user-or-llm","description":"Expected close date in YYYY-MM-DD format"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}},"hostedApiKey":"none"},"apollo_opportunity_get":{"id":"apollo_opportunity_get","name":"Apollo Get Opportunity","description":"Retrieve complete details of a specific deal/opportunity by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"opportunity_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the opportunity to retrieve (e.g., \\"opp_abc123\\")"}},"hostedApiKey":"none"},"apollo_opportunity_search":{"id":"apollo_opportunity_search","name":"Apollo Search Opportunities","description":"Search and list all deals/opportunities in your team\'s Apollo account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"amount\\", \\"is_closed\\", or \\"is_won\\""},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_opportunity_update":{"id":"apollo_opportunity_update","name":"Apollo Update Opportunity","description":"Update an existing deal/opportunity in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"opportunity_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the opportunity to update (e.g., \\"opp_abc123\\")"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the opportunity/deal (e.g., \\"Enterprise License - Q1\\")"},"amount":{"type":"string","required":false,"visibility":"user-or-llm","description":"Monetary value as a plain number string with no commas or currency symbols"},"opportunity_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the opportunity stage"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the opportunity owner"},"closed_date":{"type":"string","required":false,"visibility":"user-or-llm","description":"Expected close date in YYYY-MM-DD format"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}},"hostedApiKey":"none"},"apollo_organization_bulk_enrich":{"id":"apollo_organization_bulk_enrich","name":"Apollo Bulk Organization Enrichment","description":"Enrich data for up to 10 organizations at once using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"domains":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of company domains to enrich (max 10, no www. or @, e.g., [\\"apollo.io\\", \\"stripe.com\\"])"}},"hostedApiKey":"none"},"apollo_organization_enrich":{"id":"apollo_organization_enrich","name":"Apollo Organization Enrichment","description":"Enrich data for a single organization using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"domain":{"type":"string","required":true,"visibility":"user-or-llm","description":"Company domain (e.g., \\"apollo.io\\", \\"acme.com\\")"}},"hostedApiKey":"none"},"apollo_organization_search":{"id":"apollo_organization_search","name":"Apollo Organization Search","description":"Search Apollo\'s database for companies using filters","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"organization_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Company HQ locations (cities, US states, or countries)"},"organization_not_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Exclude companies whose HQ is in these locations"},"organization_num_employees_ranges":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employee count ranges as \\"min,max\\" strings (e.g., [\\"1,10\\", \\"250,500\\", \\"10000,20000\\"])"},"q_organization_keyword_tags":{"type":"array","required":false,"visibility":"user-or-llm","description":"Industry or keyword tags"},"q_organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Organization name to search for (e.g., \\"Acme\\", \\"TechCorp\\")"},"organization_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Apollo organization IDs to include (e.g., [\\"5e66b6381e05b4008c8331b8\\"])"},"q_organization_domains_list":{"type":"array","required":false,"visibility":"user-or-llm","description":"Domain names to filter by (no www. or @, up to 1,000)"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_people_bulk_enrich":{"id":"apollo_people_bulk_enrich","name":"Apollo Bulk People Enrichment","description":"Enrich data for up to 10 people at once using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"people":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of people to enrich (max 10)"},"reveal_personal_emails":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal personal email addresses (uses credits)"},"reveal_phone_number":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal phone numbers (uses credits, requires webhook_url)"},"webhook_url":{"type":"string","required":false,"visibility":"user-only","description":"Webhook URL for async phone number delivery (required when reveal_phone_number is true)"}},"hostedApiKey":"none"},"apollo_people_enrich":{"id":"apollo_people_enrich","name":"Apollo People Enrichment","description":"Enrich data for a single person using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"first_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"First name of the person"},"last_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Last name of the person"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Full name of the person (alternative to first_name/last_name)"},"id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the person"},"hashed_email":{"type":"string","required":false,"visibility":"user-or-llm","description":"MD5 or SHA-256 hashed email"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address of the person"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company name where the person works"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain (e.g., \\"apollo.io\\", \\"acme.com\\")"},"linkedin_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"LinkedIn profile URL"},"reveal_personal_emails":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal personal email addresses (uses credits)"},"reveal_phone_number":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal phone numbers (uses credits, requires webhook_url)"},"webhook_url":{"type":"string","required":false,"visibility":"user-only","description":"Webhook URL for async phone number delivery (required when reveal_phone_number is true)"}},"hostedApiKey":"none"},"apollo_people_search":{"id":"apollo_people_search","name":"Apollo People Search","description":"Search Apollo\'s database for people using demographic filters","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"person_titles":{"type":"array","required":false,"visibility":"user-or-llm","description":"Job titles to search for (e.g., [\\"CEO\\", \\"VP of Sales\\"])"},"include_similar_titles":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to return people with job titles similar to person_titles"},"person_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Locations to search in (e.g., [\\"San Francisco, CA\\", \\"New York, NY\\"])"},"person_seniorities":{"type":"array","required":false,"visibility":"user-or-llm","description":"Seniority levels (one of: owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern)"},"organization_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Apollo organization IDs to filter by (e.g., [\\"5e66b6381e05b4008c8331b8\\"])"},"organization_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Company names to search within (legacy filter)"},"organization_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Headquarters locations of the people\'s current employer (e.g., [\'texas\', \'tokyo\', \'spain\'])"},"q_organization_domains_list":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employer domain names (e.g., [\\"apollo.io\\", \\"microsoft.com\\"]) — up to 1,000, no www. or @"},"organization_num_employees_ranges":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employee count ranges for the person\'s current employer. Each entry is \\"min,max\\" (e.g., [\\"1,10\\", \\"250,500\\", \\"10000,20000\\"])"},"contact_email_status":{"type":"array","required":false,"visibility":"user-or-llm","description":"Email statuses to filter by: \\"verified\\", \\"unverified\\", \\"likely to engage\\", \\"unavailable\\""},"q_keywords":{"type":"string","required":false,"visibility":"user-or-llm","description":"Keywords to search for"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination, default 1 (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, default 25, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_sequence_add_contacts":{"id":"apollo_sequence_add_contacts","name":"Apollo Add Contacts to Sequence","description":"Add contacts to an Apollo sequence","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"sequence_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the sequence to add contacts to (e.g., \\"seq_abc123\\")"},"contact_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of contact IDs to add to the sequence (e.g., [\\"con_abc123\\", \\"con_def456\\"]). Either contact_ids or label_names must be provided."},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of label names to identify contacts to add to the sequence. Either contact_ids or label_names must be provided."},"send_email_from_email_account_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the email account to send from. Use the Get Email Accounts operation to look this up."},"send_email_from_email_address":{"type":"string","required":false,"visibility":"user-only","description":"Specific email address to send from within the email account."},"sequence_no_email":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if they have no email address"},"sequence_unverified_email":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts with unverified email addresses"},"sequence_job_change":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts who recently changed jobs"},"sequence_active_in_other_campaigns":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts active in other campaigns"},"sequence_finished_in_other_campaigns":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts who finished other campaigns"},"sequence_same_company_in_same_campaign":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if others from the same company are in the sequence"},"contacts_without_ownership_permission":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts without ownership permission"},"add_if_in_queue":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if they are in the queue"},"contact_verification_skipped":{"type":"boolean","required":false,"visibility":"user-only","description":"Skip contact verification when adding"},"user_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the user performing the action"},"status":{"type":"string","required":false,"visibility":"user-only","description":"Initial status for added contacts: \\"active\\" or \\"paused\\""},"auto_unpause_at":{"type":"string","required":false,"visibility":"user-only","description":"ISO 8601 datetime to automatically unpause contacts"}},"hostedApiKey":"none"},"apollo_sequence_search":{"id":"apollo_sequence_search","name":"Apollo Search Sequences","description":"Search for sequences/campaigns in your team\'s Apollo account (master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"q_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search sequences by name (e.g., \\"Outbound Q1\\", \\"Follow-up\\")"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"apollo_task_create":{"id":"apollo_task_create","name":"Apollo Create Task","description":"Create one or more tasks in Apollo (one task per contact_id, master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"user_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the Apollo user the task is assigned to"},"contact_ids":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of contact IDs. One task is created per contact."},"priority":{"type":"string","required":false,"visibility":"user-or-llm","description":"Task priority: \\"high\\", \\"medium\\", or \\"low\\" (defaults to \\"medium\\")"},"due_at":{"type":"string","required":true,"visibility":"user-or-llm","description":"Due date/time in ISO 8601 format (e.g., \\"2024-12-31T23:59:59Z\\")"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task type: \\"call\\", \\"outreach_manual_email\\", \\"linkedin_step_connect\\", \\"linkedin_step_message\\", \\"linkedin_step_view_profile\\", \\"linkedin_step_interact_post\\", or \\"action_item\\""},"status":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task status: \\"scheduled\\", \\"completed\\", or \\"skipped\\""},"note":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-form note providing context for the task"}},"hostedApiKey":"none"},"apollo_task_search":{"id":"apollo_task_search","name":"Apollo Search Tasks","description":"Search for tasks in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"task_due_at\\" or \\"task_priority\\""},"open_factor_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Filter by status. Common values: [\\"task_types\\"] for open tasks, [\\"task_completed_at\\"] for completed tasks."},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}},"hostedApiKey":"none"},"appconfig_create_application":{"id":"appconfig_create_application","name":"AppConfig Create Application","description":"Create an application in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the application to create"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the application"}},"hostedApiKey":"none"},"appconfig_create_configuration_profile":{"id":"appconfig_create_configuration_profile","name":"AppConfig Create Configuration Profile","description":"Create a configuration profile in an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to create the configuration profile in"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the configuration profile"},"locationUri":{"type":"string","required":true,"visibility":"user-or-llm","description":"Where the configuration is stored. Use \\"hosted\\" for AppConfig-hosted configurations, or an SSM/S3 URI"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the configuration profile"},"retrievalRoleArn":{"type":"string","required":false,"visibility":"user-or-llm","description":"ARN of an IAM role to retrieve the configuration (required for non-hosted URIs)"},"type":{"type":"string","required":false,"visibility":"user-or-llm","description":"Profile type: AWS.Freeform (default) or AWS.AppConfig.FeatureFlags"}},"hostedApiKey":"none"},"appconfig_create_environment":{"id":"appconfig_create_environment","name":"AppConfig Create Environment","description":"Create an environment for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to create the environment in"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the environment to create"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the environment"}},"hostedApiKey":"none"},"appconfig_create_hosted_configuration_version":{"id":"appconfig_create_hosted_configuration_version","name":"AppConfig Create Hosted Configuration Version","description":"Create a new hosted configuration version for an AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to add the version to"},"content":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration content (e.g., a JSON or YAML document)"},"contentType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Content type of the configuration (e.g., application/json, text/plain)"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the configuration version"},"latestVersionNumber":{"type":"number","required":false,"visibility":"user-or-llm","description":"The version number of the latest version, used for optimistic concurrency"},"versionLabel":{"type":"string","required":false,"visibility":"user-or-llm","description":"A user-defined label for the configuration version"}},"hostedApiKey":"none"},"appconfig_delete_application":{"id":"appconfig_delete_application","name":"AppConfig Delete Application","description":"Delete an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to delete"}},"hostedApiKey":"none"},"appconfig_delete_configuration_profile":{"id":"appconfig_delete_configuration_profile","name":"AppConfig Delete Configuration Profile","description":"Delete an AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to delete"}},"hostedApiKey":"none"},"appconfig_delete_environment":{"id":"appconfig_delete_environment","name":"AppConfig Delete Environment","description":"Delete an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to delete"}},"hostedApiKey":"none"},"appconfig_delete_hosted_configuration_version":{"id":"appconfig_delete_hosted_configuration_version","name":"AppConfig Delete Hosted Configuration Version","description":"Delete a specific hosted configuration version from an AppConfig profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID that owns the version"},"versionNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The version number to delete"}},"hostedApiKey":"none"},"appconfig_get_application":{"id":"appconfig_get_application","name":"AppConfig Get Application","description":"Get details about a single AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to retrieve"}},"hostedApiKey":"none"},"appconfig_get_configuration":{"id":"appconfig_get_configuration","name":"AppConfig Get Configuration","description":"Retrieve the latest deployed configuration for an AppConfig application, environment, and profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID or name to retrieve configuration for"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID or name to retrieve configuration for"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID or name to retrieve"}},"hostedApiKey":"none"},"appconfig_get_configuration_profile":{"id":"appconfig_get_configuration_profile","name":"AppConfig Get Configuration Profile","description":"Get details about a single AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to retrieve"}},"hostedApiKey":"none"},"appconfig_get_deployment":{"id":"appconfig_get_deployment","name":"AppConfig Get Deployment","description":"Get details about a specific AWS AppConfig deployment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployment"},"deploymentNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The sequence number of the deployment"}},"hostedApiKey":"none"},"appconfig_get_environment":{"id":"appconfig_get_environment","name":"AppConfig Get Environment","description":"Get details about a single AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to retrieve"}},"hostedApiKey":"none"},"appconfig_get_hosted_configuration_version":{"id":"appconfig_get_hosted_configuration_version","name":"AppConfig Get Hosted Configuration Version","description":"Retrieve a specific hosted configuration version from an AppConfig profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to read the version from"},"versionNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The version number to retrieve"}},"hostedApiKey":"none"},"appconfig_list_applications":{"id":"appconfig_list_applications","name":"AppConfig List Applications","description":"List applications in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of applications to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_configuration_profiles":{"id":"appconfig_list_configuration_profiles","name":"AppConfig List Configuration Profiles","description":"List configuration profiles for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profiles"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of configuration profiles to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_deployment_strategies":{"id":"appconfig_list_deployment_strategies","name":"AppConfig List Deployment Strategies","description":"List deployment strategies available in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of deployment strategies to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_deployments":{"id":"appconfig_list_deployments","name":"AppConfig List Deployments","description":"List deployments for an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployments"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployments"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of deployments to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_environments":{"id":"appconfig_list_environments","name":"AppConfig List Environments","description":"List environments for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environments"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of environments to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_list_hosted_configuration_versions":{"id":"appconfig_list_hosted_configuration_versions","name":"AppConfig List Hosted Configuration Versions","description":"List hosted configuration versions for an AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to list versions for"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of versions to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}},"hostedApiKey":"none"},"appconfig_start_deployment":{"id":"appconfig_start_deployment","name":"AppConfig Start Deployment","description":"Start deploying a configuration version to an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to deploy in"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to deploy to"},"deploymentStrategyId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The deployment strategy ID to use"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to deploy"},"configurationVersion":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration version to deploy"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the deployment"}},"hostedApiKey":"none"},"appconfig_stop_deployment":{"id":"appconfig_stop_deployment","name":"AppConfig Stop Deployment","description":"Stop an in-progress AWS AppConfig deployment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployment"},"deploymentNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The sequence number of the deployment to stop"}},"hostedApiKey":"none"},"appconfig_update_application":{"id":"appconfig_update_application","name":"AppConfig Update Application","description":"Update the name or description of an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the application"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the application"}},"hostedApiKey":"none"},"appconfig_update_configuration_profile":{"id":"appconfig_update_configuration_profile","name":"AppConfig Update Configuration Profile","description":"Update the name, description, or retrieval role of an AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the configuration profile"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the configuration profile"},"retrievalRoleArn":{"type":"string","required":false,"visibility":"user-or-llm","description":"New ARN of the IAM role used to retrieve the configuration"}},"hostedApiKey":"none"},"appconfig_update_environment":{"id":"appconfig_update_environment","name":"AppConfig Update Environment","description":"Update the name or description of an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the environment"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the environment"}},"hostedApiKey":"none"},"arxiv_get_author_papers":{"id":"arxiv_get_author_papers","name":"ArXiv Get Author Papers","description":"Search for papers by a specific author on ArXiv.","version":"1.0.0","params":{"authorName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Author name to search for"},"maxResults":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 10, max: 2000)"}},"hostedApiKey":"none"},"arxiv_get_paper":{"id":"arxiv_get_paper","name":"ArXiv Get Paper","description":"Get detailed information about a specific ArXiv paper by its ID.","version":"1.0.0","params":{"paperId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ArXiv paper ID (e.g., \\"1706.03762\\")"}},"hostedApiKey":"none"},"arxiv_search":{"id":"arxiv_search","name":"ArXiv Search","description":"Search for academic papers on ArXiv by keywords, authors, titles, or other fields.","version":"1.0.0","params":{"searchQuery":{"type":"string","required":true,"visibility":"user-or-llm","description":"The search query to execute"},"searchField":{"type":"string","required":false,"visibility":"user-only","description":"Field to search in: all, ti (title), au (author), abs (abstract), co (comment), jr (journal), cat (category), rn (report number)"},"maxResults":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 10, max: 2000)"},"sortBy":{"type":"string","required":false,"visibility":"user-only","description":"Sort by: relevance, lastUpdatedDate, submittedDate (default: relevance)"},"sortOrder":{"type":"string","required":false,"visibility":"user-only","description":"Sort order: ascending, descending (default: descending)"}},"hostedApiKey":"none"},"asana_add_comment":{"id":"asana_add_comment","name":"Asana Add Comment","description":"Add a comment (story) to an Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana task GID (numeric string)"},"text":{"type":"string","required":true,"visibility":"user-or-llm","description":"The text content of the comment"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_add_followers":{"id":"asana_add_followers","name":"Asana Add Followers","description":"Add one or more followers to an Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana task (numeric string)"},"followers":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of user GIDs to add as followers to the task"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_create_project":{"id":"asana_create_project","name":"Asana Create Project","description":"Create a new project in an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) where the project will be created"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the project"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the project"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_create_section":{"id":"asana_create_section","name":"Asana Create Section","description":"Create a new section in an Asana project","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana project (numeric string) to add the section to"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the section"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_create_subtask":{"id":"asana_create_subtask","name":"Asana Create Subtask","description":"Create a subtask under an existing Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the parent Asana task (numeric string)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the subtask"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the subtask"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"User GID to assign the subtask to"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_create_task":{"id":"asana_create_task","name":"Asana Create Task","description":"Create a new task in Asana","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) where the task will be created"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the task"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the task"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"User GID to assign the task to"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_delete_task":{"id":"asana_delete_task","name":"Asana Delete Task","description":"Delete an Asana task by its GID (moves it to the trash)","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana task to delete (numeric string)"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_get_project":{"id":"asana_get_project","name":"Asana Get Project","description":"Retrieve a single Asana project by its GID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana project GID (numeric string) to retrieve"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_get_projects":{"id":"asana_get_projects","name":"Asana Get Projects","description":"Retrieve all projects from an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to retrieve projects from"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_get_task":{"id":"asana_get_task","name":"Asana Get Task","description":"Retrieve a single task by GID or get multiple tasks with filters","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":false,"visibility":"user-or-llm","description":"The globally unique identifier (GID) of the task. If not provided, will get multiple tasks."},"workspace":{"type":"string","required":false,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to filter tasks (required when not using taskGid)"},"project":{"type":"string","required":false,"visibility":"user-or-llm","description":"Asana project GID (numeric string) to filter tasks"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of tasks to return (default: 50)"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_list_sections":{"id":"asana_list_sections","name":"Asana List Sections","description":"List all sections in an Asana project","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana project (numeric string) to list sections from"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_list_workspaces":{"id":"asana_list_workspaces","name":"Asana List Workspaces","description":"List all Asana workspaces and organizations the authenticated user belongs to","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_search_tasks":{"id":"asana_search_tasks","name":"Asana Search Tasks","description":"Search for tasks in an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to search tasks in"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Text to search for in task names"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter tasks by assignee user GID"},"projects":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of Asana project GIDs (numeric strings) to filter tasks by"},"completed":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Filter by completion status"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"asana_update_task":{"id":"asana_update_task","name":"Asana Update Task","description":"Update an existing task in Asana","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana task GID (numeric string) of the task to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated name for the task"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated notes or description for the task"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated assignee user GID"},"completed":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Mark task as completed or not completed"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"},"hostedApiKey":"none"},"ashby_add_candidate_tag":{"id":"ashby_add_candidate_tag","name":"Ashby Add Candidate Tag","description":"Adds a tag to a candidate in Ashby and returns the updated candidate.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to add the tag to"},"tagId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the tag to add"}},"hostedApiKey":"none"},"ashby_anonymize_candidate":{"id":"ashby_anonymize_candidate","name":"Ashby Anonymize Candidate","description":"Strips personally identifiable information from a candidate in Ashby. This does not delete the candidate - the record and its applications remain, with the PII removed. Ashby exposes no candidate deletion endpoint; true deletion is UI-only, restricted by role, and limited to a 10-day window. Requires the candidatesWrite permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"UUID of the candidate to anonymize"}},"hostedApiKey":"none"},"ashby_change_application_source":{"id":"ashby_change_application_source","name":"Ashby Change Application Source","description":"Changes the source attributed to an existing application, so programmatically created applications report correctly on the recruiting side. Requires the candidatesWrite permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"UUID of the application whose source should change"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to attribute the application to, as returned by List Sources. Omit only when unsetSource is true."},"unsetSource":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Set true to deliberately clear the application source. Required to unset, so that a missing or empty sourceId cannot wipe attribution by accident."}},"hostedApiKey":"none"},"ashby_change_application_stage":{"id":"ashby_change_application_stage","name":"Ashby Change Application Stage","description":"Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the application to update the stage of"},"interviewStageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the interview stage to move the application to"},"archiveReasonId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Archive reason UUID. Required when moving to an Archived stage, ignored otherwise"},"archiveEmail":{"type":"json","required":false,"visibility":"user-or-llm","description":"Archive email configuration with communicationTemplateId and optional sendAt ISO 8601 timestamp. Pass null or omit to send no archive email."}},"hostedApiKey":"none"},"ashby_create_application":{"id":"ashby_create_application","name":"Ashby Create Application","description":"Creates a new application for a candidate on a job. Optionally specify interview plan, stage, source, and credited user.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to consider for the job"},"jobId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the job to consider the candidate for"},"interviewPlanId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the interview plan to use (defaults to the job default plan)"},"interviewStageId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the interview stage to place the application in, or FirstPreInterviewScreen (defaults to the first Lead stage)"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to set on the application"},"creditedToUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the user the application is credited to"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to set as the application creation date (defaults to now)"},"applicationHistory":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional documented application history entries to create with the application"}},"hostedApiKey":"none"},"ashby_create_candidate":{"id":"ashby_create_candidate","name":"Ashby Create Candidate","description":"Creates a new candidate record in Ashby.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"The candidate full name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary email address for the candidate"},"phoneNumber":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number for the candidate"},"linkedInUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"LinkedIn profile URL"},"githubUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"GitHub profile URL"},"website":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal website URL"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to attribute the candidate to"},"creditedToUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the Ashby user to credit with sourcing this candidate"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now."},"alternateEmailAddresses":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of additional email address strings to add to the candidate, e.g. [\\"a@x.com\\",\\"b@y.com\\"]"},"location":{"type":"json","required":false,"visibility":"user-or-llm","description":"Candidate location object with optional city, region, and country"}},"hostedApiKey":"none"},"ashby_create_note":{"id":"ashby_create_note","name":"Ashby Create Note","description":"Creates a note on a candidate in Ashby. Supports plain text and HTML content (bold, italic, underline, links, lists, code).","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"onBehalfOfUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Active Ashby user UUID to attribute this mutation to; the API key must permit on-behalf-of calls"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to add the note to"},"note":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note content. If noteType is text/html, supports: , , , ,