diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/api_key_header.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/api_key_header.ts index 36125c204384b..13a5a1cc319d1 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/api_key_header.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/api_key_header.ts @@ -7,26 +7,28 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import { isString } from 'lodash'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; const HEADER_FIELD_DEFAULT = 'Api-Key'; -const authSchema = z - .object({ - headerField: z - .string() - .min(1, { message: i18n.HEADER_AUTH_REQUIRED_MESSAGE }) - .default(HEADER_FIELD_DEFAULT) - .meta({ label: i18n.HEADER_AUTH_LABEL, sensitive: true }), - apiKey: z - .string() - .min(1, { message: i18n.API_KEY_AUTH_REQUIRED_MESSAGE }) - .meta({ label: i18n.API_KEY_AUTH_LABEL, sensitive: true }), - }) - .meta({ label: i18n.API_KEY_HEADER_AUTHENTICATION_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + headerField: z + .string() + .min(1, { message: i18n.HEADER_AUTH_REQUIRED_MESSAGE }) + .default(HEADER_FIELD_DEFAULT) + .meta({ label: i18n.HEADER_AUTH_LABEL, sensitive: true }), + apiKey: z + .string() + .min(1, { message: i18n.API_KEY_AUTH_REQUIRED_MESSAGE }) + .meta({ label: i18n.API_KEY_AUTH_LABEL, sensitive: true }), + }) + .meta({ label: i18n.API_KEY_HEADER_AUTHENTICATION_LABEL }) +); type AuthSchemaType = z.infer; type NormalizedAuthSchemaType = Record; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/aws_credentials.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/aws_credentials.ts index 6c5f52ac5fc01..6f93ded662e66 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/aws_credentials.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/aws_credentials.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; @@ -17,18 +17,20 @@ import { parseAwsHost, signRequest } from './aws_credential_helpers'; // Auth Type Definition // ============================================================================ -const authSchema = z - .object({ - accessKeyId: z - .string() - .min(1, { message: i18n.AWS_ACCESS_KEY_ID_REQUIRED_MESSAGE }) - .meta({ sensitive: true, label: i18n.AWS_ACCESS_KEY_ID_LABEL }), - secretAccessKey: z - .string() - .min(1, { message: i18n.AWS_SECRET_ACCESS_KEY_REQUIRED_MESSAGE }) - .meta({ sensitive: true, label: i18n.AWS_SECRET_ACCESS_KEY_LABEL }), - }) - .meta({ label: i18n.AWS_CREDENTIALS_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + accessKeyId: z + .string() + .min(1, { message: i18n.AWS_ACCESS_KEY_ID_REQUIRED_MESSAGE }) + .meta({ sensitive: true, label: i18n.AWS_ACCESS_KEY_ID_LABEL }), + secretAccessKey: z + .string() + .min(1, { message: i18n.AWS_SECRET_ACCESS_KEY_REQUIRED_MESSAGE }) + .meta({ sensitive: true, label: i18n.AWS_SECRET_ACCESS_KEY_LABEL }), + }) + .meta({ label: i18n.AWS_CREDENTIALS_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/azure_shared_key.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/azure_shared_key.ts index 1bd662e50ad2f..485d81cdf5376 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/azure_shared_key.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/azure_shared_key.ts @@ -7,24 +7,26 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; import { computeSignature } from './azure_shared_key_crypto'; -const authSchema = z - .object({ - accountName: z - .string() - .min(1, { message: i18n.AZURE_SHARED_KEY_ACCOUNT_NAME_REQUIRED_MESSAGE }) - .meta({ label: i18n.AZURE_SHARED_KEY_ACCOUNT_NAME_LABEL }), - accountKey: z - .string() - .min(1, { message: i18n.AZURE_SHARED_KEY_ACCOUNT_KEY_REQUIRED_MESSAGE }) - .meta({ sensitive: true, label: i18n.AZURE_SHARED_KEY_ACCOUNT_KEY_LABEL }), - }) - .meta({ label: i18n.AZURE_SHARED_KEY_AUTH_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + accountName: z + .string() + .min(1, { message: i18n.AZURE_SHARED_KEY_ACCOUNT_NAME_REQUIRED_MESSAGE }) + .meta({ label: i18n.AZURE_SHARED_KEY_ACCOUNT_NAME_LABEL }), + accountKey: z + .string() + .min(1, { message: i18n.AZURE_SHARED_KEY_ACCOUNT_KEY_REQUIRED_MESSAGE }) + .meta({ sensitive: true, label: i18n.AZURE_SHARED_KEY_ACCOUNT_KEY_LABEL }), + }) + .meta({ label: i18n.AZURE_SHARED_KEY_AUTH_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/basic.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/basic.ts index faf3f1bffe080..b0e060a21871c 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/basic.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/basic.ts @@ -7,23 +7,25 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; -const authSchema = z - .object({ - username: z - .string() - .min(1, { message: i18n.BASIC_AUTH_USERNAME_REQUIRED_MESSAGE }) - .meta({ label: i18n.BASIC_AUTH_USERNAME_LABEL }), - password: z - .string() - .min(1, { message: i18n.BASIC_AUTH_PASSWORD_REQUIRED_MESSAGE }) - .meta({ sensitive: true, label: i18n.BASIC_AUTH_PASSWORD_LABEL }), - }) - .meta({ label: i18n.BASIC_AUTH_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + username: z + .string() + .min(1, { message: i18n.BASIC_AUTH_USERNAME_REQUIRED_MESSAGE }) + .meta({ label: i18n.BASIC_AUTH_USERNAME_LABEL }), + password: z + .string() + .min(1, { message: i18n.BASIC_AUTH_PASSWORD_REQUIRED_MESSAGE }) + .meta({ sensitive: true, label: i18n.BASIC_AUTH_PASSWORD_LABEL }), + }) + .meta({ label: i18n.BASIC_AUTH_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/bearer.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/bearer.ts index 48828bc3052ab..5851e25ddac04 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/bearer.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/bearer.ts @@ -7,19 +7,21 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; -const authSchema = z - .object({ - token: z - .string() - .min(1, { message: i18n.BEARER_AUTH_REQUIRED_MESSAGE }) - .meta({ sensitive: true, label: i18n.BEARER_TOKEN_LABEL }), - }) - .meta({ label: i18n.BEARER_AUTH_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + token: z + .string() + .min(1, { message: i18n.BEARER_AUTH_REQUIRED_MESSAGE }) + .meta({ sensitive: true, label: i18n.BEARER_TOKEN_LABEL }), + }) + .meta({ label: i18n.BEARER_AUTH_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/crt.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/crt.ts index d4a3205b35292..047ce25b40b45 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/crt.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/crt.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import { isString } from 'lodash'; import type { SSLSettings } from '@kbn/actions-utils'; @@ -15,21 +15,23 @@ import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; import { configureAxiosInstanceWithSsl } from '../lib'; -const authSchema = z - .object({ - crt: z.string().meta({ label: i18n.CRT_AUTH_CERT_LABEL }), - key: z.string().meta({ label: i18n.CRT_AUTH_KEY_LABEL, sensitive: true }), - passphrase: z - .string() - .meta({ label: i18n.CRT_AUTH_PASSPHRASE_LABEL, sensitive: true }) - .optional(), - ca: z.string().meta({ label: i18n.CRT_AUTH_CA_LABEL }).optional(), - verificationMode: z - .enum(['none', 'certificate', 'full']) - .meta({ label: i18n.CRT_AUTH_VERIFICATION_MODE_LABEL }) - .optional(), - }) - .meta({ label: i18n.CRT_AUTH_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + crt: z.string().meta({ label: i18n.CRT_AUTH_CERT_LABEL }), + key: z.string().meta({ label: i18n.CRT_AUTH_KEY_LABEL, sensitive: true }), + passphrase: z + .string() + .meta({ label: i18n.CRT_AUTH_PASSPHRASE_LABEL, sensitive: true }) + .optional(), + ca: z.string().meta({ label: i18n.CRT_AUTH_CA_LABEL }).optional(), + verificationMode: z + .enum(['none', 'certificate', 'full']) + .meta({ label: i18n.CRT_AUTH_VERIFICATION_MODE_LABEL }) + .optional(), + }) + .meta({ label: i18n.CRT_AUTH_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/ears.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/ears.ts index c9b72a299089c..2dac012c32329 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/ears.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/ears.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import { isConnectorAuthorizationError } from '../errors/connector_authorization_error'; @@ -17,12 +17,14 @@ import * as i18n from './translations'; export const EARS_AUTH_ID = 'ears'; export const EARS_PROVIDERS = ['google', 'microsoft', 'slack'] as const; -const authSchema = z - .object({ - provider: z.enum(EARS_PROVIDERS).meta({ hidden: true }), - scope: z.string().meta({ label: i18n.OAUTH_SCOPE_LABEL }).optional(), - }) - .meta({ label: i18n.EARS_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + provider: z.enum(EARS_PROVIDERS).meta({ hidden: true }), + scope: z.string().meta({ label: i18n.OAUTH_SCOPE_LABEL }).optional(), + }) + .meta({ label: i18n.EARS_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/gcp_service_account.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/gcp_service_account.ts index 0ef80eda79fa9..82e620139c18b 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/gcp_service_account.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/gcp_service_account.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; @@ -15,25 +15,27 @@ import { getGcpAccessToken, parseServiceAccountKey } from './gcp_jwt_helpers'; const DEFAULT_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'; -const authSchema = z - .object({ - serviceAccountJson: z - .string() - .min(1, { message: i18n.GCP_SERVICE_ACCOUNT_JSON_REQUIRED_MESSAGE }) - .meta({ - sensitive: true, - widget: 'fileUpload', - widgetOptions: { accept: '.json' }, - label: i18n.GCP_SERVICE_ACCOUNT_JSON_LABEL, - helpText: i18n.GCP_SERVICE_ACCOUNT_JSON_HELP_TEXT, +const authSchema = lazySchema(() => + z + .object({ + serviceAccountJson: z + .string() + .min(1, { message: i18n.GCP_SERVICE_ACCOUNT_JSON_REQUIRED_MESSAGE }) + .meta({ + sensitive: true, + widget: 'fileUpload', + widgetOptions: { accept: '.json' }, + label: i18n.GCP_SERVICE_ACCOUNT_JSON_LABEL, + helpText: i18n.GCP_SERVICE_ACCOUNT_JSON_HELP_TEXT, + }), + scope: z.string().optional().meta({ + label: i18n.GCP_SERVICE_ACCOUNT_SCOPE_LABEL, + helpText: i18n.GCP_SERVICE_ACCOUNT_SCOPE_HELP_TEXT, + hidden: true, }), - scope: z.string().optional().meta({ - label: i18n.GCP_SERVICE_ACCOUNT_SCOPE_LABEL, - helpText: i18n.GCP_SERVICE_ACCOUNT_SCOPE_HELP_TEXT, - hidden: true, - }), - }) - .meta({ label: i18n.GCP_SERVICE_ACCOUNT_LABEL }); + }) + .meta({ label: i18n.GCP_SERVICE_ACCOUNT_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/none.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/none.ts index ea6e36521d77c..abd2be4db8565 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/none.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/none.ts @@ -7,12 +7,12 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; -const authSchema = z.object({}).meta({ label: i18n.NO_AUTH_LABEL }); +const authSchema = lazySchema(() => z.object({}).meta({ label: i18n.NO_AUTH_LABEL })); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/oauth.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/oauth.ts index bf90ec0d5c8cb..31de81e39ea9d 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/oauth.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/oauth.ts @@ -7,29 +7,33 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; -const authSchema = z - .object({ - tokenUrl: z.url().meta({ label: i18n.OAUTH_TOKEN_URL_LABEL, validate: { allowedHosts: true } }), - clientId: z - .string() - .min(1, { message: i18n.OAUTH_CLIENT_ID_REQUIRED_MESSAGE }) - .meta({ label: i18n.OAUTH_CLIENT_ID_LABEL }), - scope: z.string().meta({ label: i18n.OAUTH_SCOPE_LABEL }).optional(), - clientSecret: z - .string() - .min(1, { message: i18n.OAUTH_CLIENT_SECRET_REQUIRED_MESSAGE }) - .meta({ label: i18n.OAUTH_CLIENT_SECRET_LABEL, sensitive: true }), - tokenEndpointAuthMethod: z - .enum(['client_secret_post', 'client_secret_basic']) - .meta({ label: i18n.OAUTH_TOKEN_ENDPOINT_AUTH_METHOD_LABEL, hidden: true }) - .optional(), - }) - .meta({ label: i18n.OAUTH_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + tokenUrl: z + .url() + .meta({ label: i18n.OAUTH_TOKEN_URL_LABEL, validate: { allowedHosts: true } }), + clientId: z + .string() + .min(1, { message: i18n.OAUTH_CLIENT_ID_REQUIRED_MESSAGE }) + .meta({ label: i18n.OAUTH_CLIENT_ID_LABEL }), + scope: z.string().meta({ label: i18n.OAUTH_SCOPE_LABEL }).optional(), + clientSecret: z + .string() + .min(1, { message: i18n.OAUTH_CLIENT_SECRET_REQUIRED_MESSAGE }) + .meta({ label: i18n.OAUTH_CLIENT_SECRET_LABEL, sensitive: true }), + tokenEndpointAuthMethod: z + .enum(['client_secret_post', 'client_secret_basic']) + .meta({ label: i18n.OAUTH_TOKEN_ENDPOINT_AUTH_METHOD_LABEL, hidden: true }) + .optional(), + }) + .meta({ label: i18n.OAUTH_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/oauth_authorization_code.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/oauth_authorization_code.ts index 71acaaaa45204..c41243bd9c495 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/oauth_authorization_code.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/oauth_authorization_code.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import { normalizeAuthorizationHeaderValue } from './oauth_authz_code_and_ears_helpers'; @@ -16,39 +16,43 @@ import * as i18n from './translations'; export const OAUTH_AUTHORIZATION_CODE_AUTH_ID = 'oauth_authorization_code'; -const authSchema = z - .object({ - authorizationUrl: z.url().meta({ - label: i18n.OAUTH_AUTHORIZATION_URL_LABEL, - validate: { allowedHosts: true }, - }), - tokenUrl: z.url().meta({ label: i18n.OAUTH_TOKEN_URL_LABEL, validate: { allowedHosts: true } }), - clientId: z - .string() - .min(1, { message: i18n.OAUTH_CLIENT_ID_REQUIRED_MESSAGE }) - .meta({ label: i18n.OAUTH_CLIENT_ID_LABEL }), - clientSecret: z - .string() - .min(1, { message: i18n.OAUTH_CLIENT_SECRET_REQUIRED_MESSAGE }) - .meta({ label: i18n.OAUTH_CLIENT_SECRET_LABEL, sensitive: true }), - scope: z.string().meta({ label: i18n.OAUTH_SCOPE_LABEL }).optional(), - useBasicAuth: z.boolean().default(true).optional().meta({ - hidden: true, // Hidden from UI - uses connector spec defaults - }), - scopeParamName: z.string().optional().meta({ - hidden: true, // Override the authorization URL query param name (falls back to 'scope') - }), - accessTokenPath: z.string().optional().meta({ - hidden: true, // JSON path for access_token in the token response (falls back to 'access_token') - }), - tokenTypePath: z.string().optional().meta({ - hidden: true, // JSON path for token_type in the token response (falls back to 'token_type') - }), - tokenType: z.string().optional().meta({ - hidden: true, // Literal token type for Authorization header, bypasses response extraction - }), - }) - .meta({ label: i18n.OAUTH_AUTHORIZATION_CODE_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + authorizationUrl: z.url().meta({ + label: i18n.OAUTH_AUTHORIZATION_URL_LABEL, + validate: { allowedHosts: true }, + }), + tokenUrl: z + .url() + .meta({ label: i18n.OAUTH_TOKEN_URL_LABEL, validate: { allowedHosts: true } }), + clientId: z + .string() + .min(1, { message: i18n.OAUTH_CLIENT_ID_REQUIRED_MESSAGE }) + .meta({ label: i18n.OAUTH_CLIENT_ID_LABEL }), + clientSecret: z + .string() + .min(1, { message: i18n.OAUTH_CLIENT_SECRET_REQUIRED_MESSAGE }) + .meta({ label: i18n.OAUTH_CLIENT_SECRET_LABEL, sensitive: true }), + scope: z.string().meta({ label: i18n.OAUTH_SCOPE_LABEL }).optional(), + useBasicAuth: z.boolean().default(true).optional().meta({ + hidden: true, // Hidden from UI - uses connector spec defaults + }), + scopeParamName: z.string().optional().meta({ + hidden: true, // Override the authorization URL query param name (falls back to 'scope') + }), + accessTokenPath: z.string().optional().meta({ + hidden: true, // JSON path for access_token in the token response (falls back to 'access_token') + }), + tokenTypePath: z.string().optional().meta({ + hidden: true, // JSON path for token_type in the token response (falls back to 'token_type') + }), + tokenType: z.string().optional().meta({ + hidden: true, // Literal token type for Authorization header, bypasses response extraction + }), + }) + .meta({ label: i18n.OAUTH_AUTHORIZATION_CODE_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/pfx.ts b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/pfx.ts index a4456443242db..e111306a8d7c4 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/auth_types/pfx.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/auth_types/pfx.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosInstance } from 'axios'; import { isString } from 'lodash'; import type { SSLSettings } from '@kbn/actions-utils'; @@ -15,20 +15,22 @@ import type { AuthContext, AuthTypeSpec } from '../connector_spec'; import * as i18n from './translations'; import { configureAxiosInstanceWithSsl } from '../lib'; -const authSchema = z - .object({ - pfx: z.string().meta({ label: i18n.PFX_AUTH_CERT_LABEL, sensitive: true }), - passphrase: z - .string() - .meta({ label: i18n.PFX_AUTH_PASSPHRASE_LABEL, sensitive: true }) - .optional(), - ca: z.string().meta({ label: i18n.PFX_AUTH_CA_LABEL }).optional(), - verificationMode: z - .enum(['none', 'certificate', 'full']) - .meta({ label: i18n.PFX_AUTH_VERIFICATION_MODE_LABEL }) - .optional(), - }) - .meta({ label: i18n.PFX_AUTH_LABEL }); +const authSchema = lazySchema(() => + z + .object({ + pfx: z.string().meta({ label: i18n.PFX_AUTH_CERT_LABEL, sensitive: true }), + passphrase: z + .string() + .meta({ label: i18n.PFX_AUTH_PASSPHRASE_LABEL, sensitive: true }) + .optional(), + ca: z.string().meta({ label: i18n.PFX_AUTH_CA_LABEL }).optional(), + verificationMode: z + .enum(['none', 'certificate', 'full']) + .meta({ label: i18n.PFX_AUTH_VERIFICATION_MODE_LABEL }) + .optional(), + }) + .meta({ label: i18n.PFX_AUTH_LABEL }) +); type AuthSchemaType = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/abuseipdb/abuseipdb.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/abuseipdb/abuseipdb.ts index 1fb858cd43649..2cde8f8182262 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/abuseipdb/abuseipdb.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/abuseipdb/abuseipdb.ts @@ -19,7 +19,7 @@ * MVP implementation focusing on core IP reputation actions. */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { i18n } from '@kbn/i18n'; import type { ConnectorSpec } from '../../connector_spec'; @@ -41,17 +41,19 @@ export const AbuseIPDBConnector: ConnectorSpec = { actions: { checkIp: { isTool: true, - input: z.object({ - ipAddress: z.ipv4().describe('IP address to check'), - maxAgeInDays: z - .number() - .int() - .min(1) - .max(365) - .optional() - .default(90) - .describe('Maximum age of reports in days'), - }), + input: lazySchema(() => + z.object({ + ipAddress: z.ipv4().describe('IP address to check'), + maxAgeInDays: z + .number() + .int() + .min(1) + .max(365) + .optional() + .default(90) + .describe('Maximum age of reports in days'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { ipAddress: string; maxAgeInDays?: number }; const response = await ctx.client.get('https://api.abuseipdb.com/api/v2/check', { @@ -73,11 +75,13 @@ export const AbuseIPDBConnector: ConnectorSpec = { reportIp: { isTool: true, - input: z.object({ - ip: z.ipv4().describe('IP address to report'), - categories: z.array(z.number().int()).min(1).describe('Abuse category IDs'), - comment: z.string().optional().describe('Additional details'), - }), + input: lazySchema(() => + z.object({ + ip: z.ipv4().describe('IP address to report'), + categories: z.array(z.number().int()).min(1).describe('Abuse category IDs'), + comment: z.string().optional().describe('Additional details'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { ip: string; categories: number[]; comment?: string }; const response = await ctx.client.post( @@ -102,9 +106,11 @@ export const AbuseIPDBConnector: ConnectorSpec = { getIpInfo: { isTool: true, - input: z.object({ - ipAddress: z.ipv4().describe('IP address to lookup'), - }), + input: lazySchema(() => + z.object({ + ipAddress: z.ipv4().describe('IP address to lookup'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { ipAddress: string }; const response = await ctx.client.get('https://api.abuseipdb.com/api/v2/check', { @@ -129,17 +135,19 @@ export const AbuseIPDBConnector: ConnectorSpec = { bulkCheck: { isTool: true, - input: z.object({ - network: z.string().describe('Network in CIDR notation'), - maxAgeInDays: z - .number() - .int() - .min(1) - .max(365) - .optional() - .default(30) - .describe('Maximum age of reports in days'), - }), + input: lazySchema(() => + z.object({ + network: z.string().describe('Network in CIDR notation'), + maxAgeInDays: z + .number() + .int() + .min(1) + .max(365) + .optional() + .default(30) + .describe('Maximum age of reports in days'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { network: string; maxAgeInDays?: number }; const response = await ctx.client.get('https://api.abuseipdb.com/api/v2/check-block', { diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/alienvault_otx/alienvault_otx.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/alienvault_otx/alienvault_otx.ts index 17633d49656f4..4acb117ad07de 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/alienvault_otx/alienvault_otx.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/alienvault_otx/alienvault_otx.ts @@ -19,7 +19,7 @@ * MVP implementation focusing on core community intelligence actions. */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { i18n } from '@kbn/i18n'; import type { ConnectorSpec } from '../../connector_spec'; @@ -41,22 +41,24 @@ export const AlienVaultOTXConnector: ConnectorSpec = { actions: { getIndicator: { isTool: true, - input: z.object({ - indicatorType: z - .enum([ - 'IPv4', - 'IPv6', - 'domain', - 'hostname', - 'url', - 'FileHash-MD5', - 'FileHash-SHA1', - 'FileHash-SHA256', - ]) - .describe('Indicator type'), - indicator: z.string().describe('Indicator value'), - section: z.string().optional().describe('Specific section to retrieve'), - }), + input: lazySchema(() => + z.object({ + indicatorType: z + .enum([ + 'IPv4', + 'IPv6', + 'domain', + 'hostname', + 'url', + 'FileHash-MD5', + 'FileHash-SHA1', + 'FileHash-SHA256', + ]) + .describe('Indicator type'), + indicator: z.string().describe('Indicator value'), + section: z.string().optional().describe('Specific section to retrieve'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { indicatorType: string; indicator: string; section?: string }; const section = typedInput.section || 'general'; @@ -73,11 +75,20 @@ export const AlienVaultOTXConnector: ConnectorSpec = { searchPulses: { isTool: true, - input: z.object({ - query: z.string().optional().describe('Search query'), - page: z.number().int().min(1).optional().default(1).describe('Page number'), - limit: z.number().int().min(1).max(100).optional().default(20).describe('Results per page'), - }), + input: lazySchema(() => + z.object({ + query: z.string().optional().describe('Search query'), + page: z.number().int().min(1).optional().default(1).describe('Page number'), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .default(20) + .describe('Results per page'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { query?: string; page?: number; limit?: number }; const response = await ctx.client.get( @@ -100,9 +111,11 @@ export const AlienVaultOTXConnector: ConnectorSpec = { getPulse: { isTool: true, - input: z.object({ - pulseId: z.string().describe('Pulse ID'), - }), + input: lazySchema(() => + z.object({ + pulseId: z.string().describe('Pulse ID'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { pulseId: string }; const response = await ctx.client.get( @@ -123,21 +136,23 @@ export const AlienVaultOTXConnector: ConnectorSpec = { getRelatedPulses: { isTool: true, - input: z.object({ - indicatorType: z - .enum([ - 'IPv4', - 'IPv6', - 'domain', - 'hostname', - 'url', - 'FileHash-MD5', - 'FileHash-SHA1', - 'FileHash-SHA256', - ]) - .describe('Indicator type'), - indicator: z.string().describe('Indicator value'), - }), + input: lazySchema(() => + z.object({ + indicatorType: z + .enum([ + 'IPv4', + 'IPv6', + 'domain', + 'hostname', + 'url', + 'FileHash-MD5', + 'FileHash-SHA1', + 'FileHash-SHA256', + ]) + .describe('Indicator type'), + indicator: z.string().describe('Indicator value'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { indicatorType: string; indicator: string }; const response = await ctx.client.get( diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/amazon_s3/amazon_s3.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/amazon_s3/amazon_s3.ts index 19a241b3883b0..807778f0d0a6d 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/amazon_s3/amazon_s3.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/amazon_s3/amazon_s3.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { type ConnectorSpec } from '../../connector_spec'; import { downloadAmazonS3BucketObject, @@ -44,41 +44,45 @@ export const AmazonS3: ConnectorSpec = { auth: { types: ['aws_credentials'], }, - schema: z.object({ - region: z - .string() - .min(1) - .describe('AWS region') - .meta({ - widget: 'text', - label: i18n.translate('core.kibanaConnectorSpecs.amazonS3.config.region.label', { - defaultMessage: 'AWS Region', + schema: lazySchema(() => + z.object({ + region: z + .string() + .min(1) + .describe('AWS region') + .meta({ + widget: 'text', + label: i18n.translate('core.kibanaConnectorSpecs.amazonS3.config.region.label', { + defaultMessage: 'AWS Region', + }), + helpText: i18n.translate('core.kibanaConnectorSpecs.amazonS3.config.region.helpText', { + defaultMessage: + 'The AWS Region where your S3 buckets are located (for example, us-east-1)', + }), }), - helpText: i18n.translate('core.kibanaConnectorSpecs.amazonS3.config.region.helpText', { - defaultMessage: - 'The AWS Region where your S3 buckets are located (for example, us-east-1)', - }), - }), - }), + }) + ), actions: { listBuckets: { isTool: true, description: 'List available Amazon S3 buckets. Use this to discover which buckets exist before listing objects or downloading files.', - input: z.object({ - region: z - .string() - .optional() - .describe( - 'The AWS region to list buckets from. If not specified, buckets from the default region in the authorization credentials will be listed. Example: "us-east-1".' - ), - prefix: z - .string() - .optional() - .describe( - 'An optional prefix to filter bucket names. Only buckets whose names start with this prefix will be returned. Example: "my-app-" to find "my-app-logs" and "my-app-data".' - ), - }), + input: lazySchema(() => + z.object({ + region: z + .string() + .optional() + .describe( + 'The AWS region to list buckets from. If not specified, buckets from the default region in the authorization credentials will be listed. Example: "us-east-1".' + ), + prefix: z + .string() + .optional() + .describe( + 'An optional prefix to filter bucket names. Only buckets whose names start with this prefix will be returned. Example: "my-app-" to find "my-app-logs" and "my-app-data".' + ), + }) + ), handler: async (ctx, input: ActionListBucketsInput) => { let buckets: { name?: string; creationDate?: string }[] = []; @@ -108,39 +112,41 @@ export const AmazonS3: ConnectorSpec = { isTool: true, description: 'List objects (files and folders) in an Amazon S3 bucket. Supports filtering by prefix and pagination via continuation tokens.', - input: z.object({ - bucket: z - .string() - .min(1) - .describe('The name of the S3 bucket to list objects from. Example: "my-app-data".'), - region: z - .string() - .optional() - .describe( - 'The region of the S3 bucket. If not specified, will attempt to auto-detect. Example: "us-west-2".' - ), - prefix: z - .string() - .optional() - .describe( - 'An optional prefix to filter object keys (file paths) in the bucket. Use this to list objects under a specific folder path. Example: "logs/2024/" to list only objects in that path.' - ), - continuationToken: z - .string() - .optional() - .describe( - 'The continuation token for retrieving the next page of results. Obtain this from the "nextContinuationToken" field of a previous response when "isTruncated" is true. Omit on the first request.' - ), - maxKeys: z - .number() - .int() - .positive() - .optional() - .describe( - 'Maximum number of object keys to return in a single page. Defaults to 1000. Maximum allowed is 1000.' - ) - .default(1000), - }), + input: lazySchema(() => + z.object({ + bucket: z + .string() + .min(1) + .describe('The name of the S3 bucket to list objects from. Example: "my-app-data".'), + region: z + .string() + .optional() + .describe( + 'The region of the S3 bucket. If not specified, will attempt to auto-detect. Example: "us-west-2".' + ), + prefix: z + .string() + .optional() + .describe( + 'An optional prefix to filter object keys (file paths) in the bucket. Use this to list objects under a specific folder path. Example: "logs/2024/" to list only objects in that path.' + ), + continuationToken: z + .string() + .optional() + .describe( + 'The continuation token for retrieving the next page of results. Obtain this from the "nextContinuationToken" field of a previous response when "isTruncated" is true. Omit on the first request.' + ), + maxKeys: z + .number() + .int() + .positive() + .optional() + .describe( + 'Maximum number of object keys to return in a single page. Defaults to 1000. Maximum allowed is 1000.' + ) + .default(1000), + }) + ), handler: async (ctx, input: ActionListBucketObjectsInput) => { return (await listAmazonS3BucketObjects( ctx, @@ -157,27 +163,29 @@ export const AmazonS3: ConnectorSpec = { isTool: true, description: 'Download a file from an Amazon S3 bucket. If the file content is small enough, returns the file content directly. If the file exceeds the size limit, returns a pre-signed URL for direct download from S3 instead.', - input: z.object({ - bucket: z - .string() - .min(1) - .describe( - 'The name of the S3 bucket containing the file to download. Example: "my-app-data".' - ), - key: z - .string() - .min(1) - .describe( - 'The key (full path) of the file to download from the S3 bucket. Example: "reports/2024/summary.pdf".' - ), - maximumDownloadSizeBytes: z - .number() - .positive() - .optional() - .describe( - 'Maximum file size in bytes that can be downloaded for the file content. If the file size exceeds this limit, a pre-signed URL will be returned for direct download from S3 instead of the content. Default is 131072 (128 KB). Do not override this unless necessary to complete the task, as large files may exceed the token limit.' - ), - }), + input: lazySchema(() => + z.object({ + bucket: z + .string() + .min(1) + .describe( + 'The name of the S3 bucket containing the file to download. Example: "my-app-data".' + ), + key: z + .string() + .min(1) + .describe( + 'The key (full path) of the file to download from the S3 bucket. Example: "reports/2024/summary.pdf".' + ), + maximumDownloadSizeBytes: z + .number() + .positive() + .optional() + .describe( + 'Maximum file size in bytes that can be downloaded for the file content. If the file size exceeds this limit, a pre-signed URL will be returned for direct download from S3 instead of the content. Default is 131072 (128 KB). Do not override this unless necessary to complete the task, as large files may exceed the token limit.' + ), + }) + ), handler: async (ctx, input: ActionDownloadFileInput) => { const metadata = await getAmazonS3BucketObjectMetadata(ctx, input.bucket, input.key); if (!metadata) { diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/confluence_cloud/confluence.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/confluence_cloud/confluence.ts index 4bcaf2e572fd1..59a051800c313 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/confluence_cloud/confluence.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/confluence_cloud/confluence.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ActionContext, ConnectorSpec } from '../../../..'; import { ListPagesInputSchema, @@ -89,32 +89,37 @@ export const ConfluenceCloudConnector: ConnectorSpec = { }, ], }, - schema: z.object({ - subdomain: z - .string() - .trim() - .min(1) - .regex(BARE_SUBDOMAIN_REGEX, { - message: - 'Subdomain may only contain letters, numbers, and hyphens (for example, your-domain)', - }) - .describe( - i18n.translate('core.kibanaConnectorSpecs.confluence.config.subdomain.description', { - defaultMessage: 'Your Atlassian subdomain', + schema: lazySchema(() => + z.object({ + subdomain: z + .string() + .trim() + .min(1) + .regex(BARE_SUBDOMAIN_REGEX, { + message: + 'Subdomain may only contain letters, numbers, and hyphens (for example, your-domain)', }) - ) - .meta({ - widget: 'text', - label: i18n.translate('core.kibanaConnectorSpecs.confluence.config.subdomain.label', { - defaultMessage: 'Subdomain', + .describe( + i18n.translate('core.kibanaConnectorSpecs.confluence.config.subdomain.description', { + defaultMessage: 'Your Atlassian subdomain', + }) + ) + .meta({ + widget: 'text', + label: i18n.translate('core.kibanaConnectorSpecs.confluence.config.subdomain.label', { + defaultMessage: 'Subdomain', + }), + placeholder: 'your-domain', + helpText: i18n.translate( + 'core.kibanaConnectorSpecs.confluence.config.subdomain.helpText', + { + defaultMessage: + 'The subdomain for your Confluence Cloud site (for example, your-domain for https://your-domain.atlassian.net)', + } + ), }), - placeholder: 'your-domain', - helpText: i18n.translate('core.kibanaConnectorSpecs.confluence.config.subdomain.helpText', { - defaultMessage: - 'The subdomain for your Confluence Cloud site (for example, your-domain for https://your-domain.atlassian.net)', - }), - }), - }), + }) + ), actions: { listPages: { description: diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/confluence_cloud/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/confluence_cloud/types.ts index ff919e280e980..0377b4fae710e 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/confluence_cloud/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/confluence_cloud/types.ts @@ -7,108 +7,116 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Action input schemas & inferred types // ============================================================================= -export const ListPagesInputSchema = z.object({ - limit: z - .number() - .default(25) - .describe('Maximum number of pages to return per request. Defaults to 25 if omitted.'), - cursor: z - .string() - .optional() - .describe( - 'Opaque pagination cursor returned by a previous listPages response. Pass this to retrieve the next page of results.' - ), - spaceId: z - .union([z.string(), z.array(z.string())]) - .optional() - .describe( - 'Numeric space ID or array of space IDs to restrict results to pages in those spaces. Obtain space IDs from listSpaces or getSpace.' - ), - title: z - .string() - .optional() - .describe('Filter pages whose title contains this string (partial, case-insensitive match).'), - status: z - .union([z.string(), z.array(z.string())]) - .optional() - .describe( - 'Filter by page status. Accepted values: "current" (published), "archived", "draft". Accepts a single value or an array.' - ), - bodyFormat: z - .string() - .optional() - .describe( - 'Format to use for page body content in the response. Common values: "atlas_doc_format" (Atlassian Document Format JSON), "storage" (XML storage format). Omit to exclude body content from the response.' - ), -}); +export const ListPagesInputSchema = lazySchema(() => + z.object({ + limit: z + .number() + .default(25) + .describe('Maximum number of pages to return per request. Defaults to 25 if omitted.'), + cursor: z + .string() + .optional() + .describe( + 'Opaque pagination cursor returned by a previous listPages response. Pass this to retrieve the next page of results.' + ), + spaceId: z + .union([z.string(), z.array(z.string())]) + .optional() + .describe( + 'Numeric space ID or array of space IDs to restrict results to pages in those spaces. Obtain space IDs from listSpaces or getSpace.' + ), + title: z + .string() + .optional() + .describe('Filter pages whose title contains this string (partial, case-insensitive match).'), + status: z + .union([z.string(), z.array(z.string())]) + .optional() + .describe( + 'Filter by page status. Accepted values: "current" (published), "archived", "draft". Accepts a single value or an array.' + ), + bodyFormat: z + .string() + .optional() + .describe( + 'Format to use for page body content in the response. Common values: "atlas_doc_format" (Atlassian Document Format JSON), "storage" (XML storage format). Omit to exclude body content from the response.' + ), + }) +); export type ListPagesInput = z.infer; -export const GetPageInputSchema = z.object({ - id: z - .string() - .trim() - .min(1) - .describe( - 'The numeric ID of the Confluence page to retrieve (for example, "123456"). Obtain this from a listPages call or from the page URL.' - ), - bodyFormat: z - .string() - .optional() - .describe( - 'Format to use for page body content in the response. Common values: "atlas_doc_format" (Atlassian Document Format JSON), "storage" (XML storage format). Omit to exclude body content from the response.' - ), -}); +export const GetPageInputSchema = lazySchema(() => + z.object({ + id: z + .string() + .trim() + .min(1) + .describe( + 'The numeric ID of the Confluence page to retrieve (for example, "123456"). Obtain this from a listPages call or from the page URL.' + ), + bodyFormat: z + .string() + .optional() + .describe( + 'Format to use for page body content in the response. Common values: "atlas_doc_format" (Atlassian Document Format JSON), "storage" (XML storage format). Omit to exclude body content from the response.' + ), + }) +); export type GetPageInput = z.infer; -export const ListSpacesInputSchema = z.object({ - limit: z - .number() - .default(25) - .describe('Maximum number of spaces to return per request. Defaults to 25 if omitted.'), - cursor: z - .string() - .optional() - .describe( - 'Opaque pagination cursor returned by a previous listSpaces response. Pass this to retrieve the next page of results.' - ), - ids: z - .union([z.string(), z.array(z.string())]) - .optional() - .describe( - 'Numeric space ID or array of space IDs to retrieve specific spaces. Use when you already know the space IDs.' - ), - keys: z - .union([z.string(), z.array(z.string())]) - .optional() - .describe( - 'Space key or array of space keys to filter by (for example, "DEMO" or ["DEMO", "TEAM"]). Space keys are the short uppercase identifiers shown in Confluence URLs.' - ), - type: z - .string() - .optional() - .describe( - 'Filter spaces by type. Accepted values: "global" (team or project spaces), "personal" (user personal spaces).' - ), - status: z - .string() - .optional() - .describe('Filter spaces by status. Accepted values: "current" (active), "archived".'), -}); +export const ListSpacesInputSchema = lazySchema(() => + z.object({ + limit: z + .number() + .default(25) + .describe('Maximum number of spaces to return per request. Defaults to 25 if omitted.'), + cursor: z + .string() + .optional() + .describe( + 'Opaque pagination cursor returned by a previous listSpaces response. Pass this to retrieve the next page of results.' + ), + ids: z + .union([z.string(), z.array(z.string())]) + .optional() + .describe( + 'Numeric space ID or array of space IDs to retrieve specific spaces. Use when you already know the space IDs.' + ), + keys: z + .union([z.string(), z.array(z.string())]) + .optional() + .describe( + 'Space key or array of space keys to filter by (for example, "DEMO" or ["DEMO", "TEAM"]). Space keys are the short uppercase identifiers shown in Confluence URLs.' + ), + type: z + .string() + .optional() + .describe( + 'Filter spaces by type. Accepted values: "global" (team or project spaces), "personal" (user personal spaces).' + ), + status: z + .string() + .optional() + .describe('Filter spaces by status. Accepted values: "current" (active), "archived".'), + }) +); export type ListSpacesInput = z.infer; -export const GetSpaceInputSchema = z.object({ - id: z - .string() - .trim() - .min(1) - .describe( - 'The numeric ID of the Confluence space to retrieve (for example, "98304"). Obtain this from a listSpaces call or from the space URL.' - ), -}); +export const GetSpaceInputSchema = lazySchema(() => + z.object({ + id: z + .string() + .trim() + .min(1) + .describe( + 'The numeric ID of the Confluence space to retrieve (for example, "98304"). Obtain this from a listSpaces call or from the space URL.' + ), + }) +); export type GetSpaceInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/jira-cloud/jira.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/jira-cloud/jira.ts index 790c4ec76650e..499c9de4ed73c 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/jira-cloud/jira.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/jira-cloud/jira.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { GetIssueInput, GetProjectInput, @@ -88,45 +88,47 @@ export const JiraConnector: ConnectorSpec = { }, ], }, - schema: z.object({ - subdomain: z - .string() - .min(1) - .describe( - i18n.translate('core.kibanaConnectorSpecs.jira.config.subdomain.description', { - defaultMessage: 'Your Atlassian subdomain', - }) - ) - .meta({ - widget: 'text', - label: i18n.translate('core.kibanaConnectorSpecs.jira.config.subdomain.label', { - defaultMessage: 'Subdomain', + schema: lazySchema(() => + z.object({ + subdomain: z + .string() + .min(1) + .describe( + i18n.translate('core.kibanaConnectorSpecs.jira.config.subdomain.description', { + defaultMessage: 'Your Atlassian subdomain', + }) + ) + .meta({ + widget: 'text', + label: i18n.translate('core.kibanaConnectorSpecs.jira.config.subdomain.label', { + defaultMessage: 'Subdomain', + }), + placeholder: 'your-domain', + helpText: i18n.translate('core.kibanaConnectorSpecs.jira.config.subdomain.helpText', { + defaultMessage: + 'The subdomain for your Jira Cloud site (e.g. your-domain for https://your-domain.atlassian.net)', + }), }), - placeholder: 'your-domain', - helpText: i18n.translate('core.kibanaConnectorSpecs.jira.config.subdomain.helpText', { - defaultMessage: - 'The subdomain for your Jira Cloud site (e.g. your-domain for https://your-domain.atlassian.net)', + cloudId: z + .string() + .optional() + .describe( + i18n.translate('core.kibanaConnectorSpecs.jira.config.cloudId.description', { + defaultMessage: 'Atlassian cloud ID (OAuth)', + }) + ) + .meta({ + widget: 'text', + label: i18n.translate('core.kibanaConnectorSpecs.jira.config.cloudId.label', { + defaultMessage: 'Cloud ID', + }), + helpText: i18n.translate('core.kibanaConnectorSpecs.jira.config.cloudId.helpText', { + defaultMessage: + 'Required for OAuth. To find your Cloud ID, visit https://your-subdomain.atlassian.net/_edge/tenant_info (replace your-subdomain with your Atlassian subdomain) and use the cloudId value from the response.', + }), }), - }), - cloudId: z - .string() - .optional() - .describe( - i18n.translate('core.kibanaConnectorSpecs.jira.config.cloudId.description', { - defaultMessage: 'Atlassian cloud ID (OAuth)', - }) - ) - .meta({ - widget: 'text', - label: i18n.translate('core.kibanaConnectorSpecs.jira.config.cloudId.label', { - defaultMessage: 'Cloud ID', - }), - helpText: i18n.translate('core.kibanaConnectorSpecs.jira.config.cloudId.helpText', { - defaultMessage: - 'Required for OAuth. To find your Cloud ID, visit https://your-subdomain.atlassian.net/_edge/tenant_info (replace your-subdomain with your Atlassian subdomain) and use the cloudId value from the response.', - }), - }), - }), + }) + ), actions: { searchIssuesWithJql: { isTool: true, diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/jira-cloud/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/jira-cloud/types.ts index b396ca68f1bfb..f246b78c3c124 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/jira-cloud/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/atlassian/jira-cloud/types.ts @@ -7,90 +7,102 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Action input schemas & inferred types // ============================================================================= -export const SearchIssuesWithJqlInputSchema = z.object({ - jql: z - .string() - .describe( - 'JQL query string to filter issues. ' + - 'Operators: = != ~ (contains) IN NOT IN > >= < <=. Combine with AND/OR. ' + - 'Date functions: startOfDay(), endOfDay(), startOfWeek(), endOfWeek(), startOfMonth(). ' + - 'Use ORDER BY to sort, e.g. ORDER BY updated DESC. ' + - 'Examples: "project = PROJ AND status = \\"In Progress\\"", ' + - '"assignee = currentUser() AND priority = High", ' + - '"created >= -7d ORDER BY created DESC", ' + - '"project = PROJ AND labels = \\"bug\\" AND status != Done". ' + - 'To filter by user, get accountId from searchUsers and use: assignee = "accountId".' - ), - maxResults: z - .number() - .optional() - .describe('Maximum number of issues to return per page (default determined by Jira API)'), - nextPageToken: z - .string() - .optional() - .describe('Pagination token from a previous response to fetch the next page of results'), -}); +export const SearchIssuesWithJqlInputSchema = lazySchema(() => + z.object({ + jql: z + .string() + .describe( + 'JQL query string to filter issues. ' + + 'Operators: = != ~ (contains) IN NOT IN > >= < <=. Combine with AND/OR. ' + + 'Date functions: startOfDay(), endOfDay(), startOfWeek(), endOfWeek(), startOfMonth(). ' + + 'Use ORDER BY to sort, e.g. ORDER BY updated DESC. ' + + 'Examples: "project = PROJ AND status = \\"In Progress\\"", ' + + '"assignee = currentUser() AND priority = High", ' + + '"created >= -7d ORDER BY created DESC", ' + + '"project = PROJ AND labels = \\"bug\\" AND status != Done". ' + + 'To filter by user, get accountId from searchUsers and use: assignee = "accountId".' + ), + maxResults: z + .number() + .optional() + .describe('Maximum number of issues to return per page (default determined by Jira API)'), + nextPageToken: z + .string() + .optional() + .describe('Pagination token from a previous response to fetch the next page of results'), + }) +); export type SearchIssuesWithJqlInput = z.infer; -export const GetIssueInputSchema = z.object({ - issueId: z.string().describe('Issue key (e.g., PROJ-123) or numeric issue ID (e.g., 10042)'), -}); +export const GetIssueInputSchema = lazySchema(() => + z.object({ + issueId: z.string().describe('Issue key (e.g., PROJ-123) or numeric issue ID (e.g., 10042)'), + }) +); export type GetIssueInput = z.infer; -export const GetProjectsInputSchema = z.object({ - maxResults: z - .number() - .optional() - .describe('Maximum number of projects to return (default determined by Jira API)'), - startAt: z - .number() - .optional() - .describe('Zero-based index of the first project to return, for pagination (e.g., 0, 20, 40)'), - query: z - .string() - .optional() - .describe( - 'Text to filter projects by name or key (e.g., "Marketing" or "MKTG"). Leave empty to list all projects.' - ), -}); +export const GetProjectsInputSchema = lazySchema(() => + z.object({ + maxResults: z + .number() + .optional() + .describe('Maximum number of projects to return (default determined by Jira API)'), + startAt: z + .number() + .optional() + .describe( + 'Zero-based index of the first project to return, for pagination (e.g., 0, 20, 40)' + ), + query: z + .string() + .optional() + .describe( + 'Text to filter projects by name or key (e.g., "Marketing" or "MKTG"). Leave empty to list all projects.' + ), + }) +); export type GetProjectsInput = z.infer; -export const GetProjectInputSchema = z.object({ - projectId: z.string().describe('Project key (e.g., PROJ) or numeric project ID (e.g., 10000)'), -}); +export const GetProjectInputSchema = lazySchema(() => + z.object({ + projectId: z.string().describe('Project key (e.g., PROJ) or numeric project ID (e.g., 10000)'), + }) +); export type GetProjectInput = z.infer; -export const SearchUsersInputSchema = z.object({ - query: z - .string() - .optional() - .describe( - 'Free-text search string matched against display name, username, or email (e.g., "john.doe" or "John")' - ), - username: z.string().optional().describe("User's username or email address for exact lookup"), - accountId: z - .string() - .optional() - .describe("User's Atlassian account ID for exact lookup (e.g., 5b10ac8d82e05b22cc7d4ef5)"), - startAt: z - .number() - .optional() - .describe('Zero-based index of the first user to return, for pagination (e.g., 0, 10, 20)'), - maxResults: z - .number() - .optional() - .describe('Maximum number of users to return (default determined by Jira API)'), - property: z - .string() - .optional() - .describe( - 'A query string used to search user properties. Property keys and values must not exceed 100 characters.' - ), -}); +export const SearchUsersInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .optional() + .describe( + 'Free-text search string matched against display name, username, or email (e.g., "john.doe" or "John")' + ), + username: z.string().optional().describe("User's username or email address for exact lookup"), + accountId: z + .string() + .optional() + .describe("User's Atlassian account ID for exact lookup (e.g., 5b10ac8d82e05b22cc7d4ef5)"), + startAt: z + .number() + .optional() + .describe('Zero-based index of the first user to return, for pagination (e.g., 0, 10, 20)'), + maxResults: z + .number() + .optional() + .describe('Maximum number of users to return (default determined by Jira API)'), + property: z + .string() + .optional() + .describe( + 'A query string used to search user properties. Property keys and values must not exceed 100 characters.' + ), + }) +); export type SearchUsersInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/aws_lambda/aws_lambda.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/aws_lambda/aws_lambda.ts index e25dc620dce49..cda1c3731fb67 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/aws_lambda/aws_lambda.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/aws_lambda/aws_lambda.ts @@ -21,7 +21,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ActionContext, ConnectorSpec } from '../../connector_spec'; interface LambdaApiResponse { @@ -119,36 +119,40 @@ export const AwsLambdaConnector: ConnectorSpec = { types: ['aws_credentials'], }, - schema: z.object({ - region: z - .string() - .min(1) - .describe( - i18n.translate('connectorSpecs.awsLambda.config.region', { - defaultMessage: 'AWS Region (e.g., us-east-1, eu-west-1)', - }) - ) - .meta({ - widget: 'text', - label: i18n.translate('connectorSpecs.awsLambda.config.region.label', { - defaultMessage: 'AWS Region', + schema: lazySchema(() => + z.object({ + region: z + .string() + .min(1) + .describe( + i18n.translate('connectorSpecs.awsLambda.config.region', { + defaultMessage: 'AWS Region (e.g., us-east-1, eu-west-1)', + }) + ) + .meta({ + widget: 'text', + label: i18n.translate('connectorSpecs.awsLambda.config.region.label', { + defaultMessage: 'AWS Region', + }), + placeholder: 'us-east-1', }), - placeholder: 'us-east-1', - }), - }), + }) + ), actions: { invoke: { isTool: true, - input: z.object({ - functionName: z.string().min(1).describe('Lambda function name or ARN'), - payload: z.unknown().optional().describe('JSON payload to send to the function'), - invocationType: z - .enum(['RequestResponse', 'Event', 'DryRun']) - .default('RequestResponse') - .describe('Invocation type: RequestResponse (sync), Event (async), or DryRun'), - qualifier: z.string().optional().describe('Function version or alias to invoke'), - }), + input: lazySchema(() => + z.object({ + functionName: z.string().min(1).describe('Lambda function name or ARN'), + payload: z.unknown().optional().describe('JSON payload to send to the function'), + invocationType: z + .enum(['RequestResponse', 'Event', 'DryRun']) + .default('RequestResponse') + .describe('Invocation type: RequestResponse (sync), Event (async), or DryRun'), + qualifier: z.string().optional().describe('Function version or alias to invoke'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { functionName: string; @@ -206,10 +210,15 @@ export const AwsLambdaConnector: ConnectorSpec = { listFunctions: { isTool: true, - input: z.object({ - maxItems: z.number().optional().describe('Maximum number of functions to return (1-10000)'), - marker: z.string().optional().describe('Pagination token from a previous response'), - }), + input: lazySchema(() => + z.object({ + maxItems: z + .number() + .optional() + .describe('Maximum number of functions to return (1-10000)'), + marker: z.string().optional().describe('Pagination token from a previous response'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { maxItems?: number; @@ -247,10 +256,12 @@ export const AwsLambdaConnector: ConnectorSpec = { getFunction: { isTool: true, - input: z.object({ - functionName: z.string().min(1).describe('Lambda function name or ARN'), - qualifier: z.string().optional().describe('Function version or alias'), - }), + input: lazySchema(() => + z.object({ + functionName: z.string().min(1).describe('Lambda function name or ARN'), + qualifier: z.string().optional().describe('Function version or alias'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { functionName: string; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/azure_blob/azure_blob.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/azure_blob/azure_blob.ts index e0e837b2f4dac..c8f3182d278ed 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/azure_blob/azure_blob.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/azure_blob/azure_blob.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ActionContext, ConnectorSpec } from '../../connector_spec'; const AZURE_BLOB_API_VERSION = '2021-06-08'; @@ -48,15 +48,17 @@ function extractNextMarker(xml: string): string | undefined { * Extracts and throws a meaningful error from Azure Blob Storage API responses. * Uses the x-ms-error-code response header when available for a human-readable code. */ -const AxiosErrorSchema = z.object({ - response: z - .object({ - status: z.number().optional(), - headers: z.record(z.string(), z.string()).optional(), - }) - .optional(), - message: z.string().optional(), -}); +const AxiosErrorSchema = lazySchema(() => + z.object({ + response: z + .object({ + status: z.number().optional(), + headers: z.record(z.string(), z.string()).optional(), + }) + .optional(), + message: z.string().optional(), + }) +); function createAzureBlobError(error: unknown): Error { const parsed = AxiosErrorSchema.safeParse(error); @@ -137,51 +139,58 @@ export const AzureBlob: ConnectorSpec = { }, }, - schema: z.object({ - accountUrl: z - .string() - .min(1) - .describe( - i18n.translate('core.kibanaConnectorSpecs.azureBlob.config.accountUrl.description', { - defaultMessage: 'Azure Blob Storage account URL', - }) - ) - .meta({ - widget: 'text', - label: i18n.translate('core.kibanaConnectorSpecs.azureBlob.config.accountUrl.label', { - defaultMessage: 'Storage account URL', - }), - placeholder: 'https://myaccount.blob.core.windows.net', - helpText: i18n.translate('core.kibanaConnectorSpecs.azureBlob.config.accountUrl.helpText', { - defaultMessage: - 'The blob service endpoint, for example https://myaccount.blob.core.windows.net.', + schema: lazySchema(() => + z.object({ + accountUrl: z + .string() + .min(1) + .describe( + i18n.translate('core.kibanaConnectorSpecs.azureBlob.config.accountUrl.description', { + defaultMessage: 'Azure Blob Storage account URL', + }) + ) + .meta({ + widget: 'text', + label: i18n.translate('core.kibanaConnectorSpecs.azureBlob.config.accountUrl.label', { + defaultMessage: 'Storage account URL', + }), + placeholder: 'https://myaccount.blob.core.windows.net', + helpText: i18n.translate( + 'core.kibanaConnectorSpecs.azureBlob.config.accountUrl.helpText', + { + defaultMessage: + 'The blob service endpoint, for example https://myaccount.blob.core.windows.net.', + } + ), }), - }), - }), + }) + ), actions: { listContainers: { isTool: true, description: 'List all containers in the Azure Blob Storage account. Supports optional prefix filtering and cursor-based pagination via marker.', - input: z.object({ - prefix: z - .string() - .optional() - .describe( - 'Optional prefix to filter containers by name. Only containers whose names begin with this string are returned.' - ), - maxresults: z - .number() - .optional() - .describe('Maximum number of containers to return. Omit to use the service default.'), - marker: z - .string() - .optional() - .describe( - 'Pagination cursor returned as nextMarker from a previous listContainers response. Pass this to retrieve the next page.' - ), - }), + input: lazySchema(() => + z.object({ + prefix: z + .string() + .optional() + .describe( + 'Optional prefix to filter containers by name. Only containers whose names begin with this string are returned.' + ), + maxresults: z + .number() + .optional() + .describe('Maximum number of containers to return. Omit to use the service default.'), + marker: z + .string() + .optional() + .describe( + 'Pagination cursor returned as nextMarker from a previous listContainers response. Pass this to retrieve the next page.' + ), + }) + ), handler: async (ctx, input) => { try { const baseUrl = getBaseUrl(ctx); @@ -206,27 +215,29 @@ export const AzureBlob: ConnectorSpec = { isTool: true, description: 'List blobs inside a specific Azure Blob Storage container. Supports optional prefix filtering and cursor-based pagination.', - input: z.object({ - container: z - .string() - .describe('The name of the container to list blobs from. Example: "my-container"'), - prefix: z - .string() - .optional() - .describe( - 'Optional prefix to filter blobs by name. Only blobs whose names begin with this string are returned. Example: "logs/2024/"' - ), - maxresults: z - .number() - .optional() - .describe('Maximum number of blobs to return. Omit to use the service default.'), - marker: z - .string() - .optional() - .describe( - 'Pagination cursor returned as nextMarker from a previous listBlobs response. Pass this to retrieve the next page.' - ), - }), + input: lazySchema(() => + z.object({ + container: z + .string() + .describe('The name of the container to list blobs from. Example: "my-container"'), + prefix: z + .string() + .optional() + .describe( + 'Optional prefix to filter blobs by name. Only blobs whose names begin with this string are returned. Example: "logs/2024/"' + ), + maxresults: z + .number() + .optional() + .describe('Maximum number of blobs to return. Omit to use the service default.'), + marker: z + .string() + .optional() + .describe( + 'Pagination cursor returned as nextMarker from a previous listBlobs response. Pass this to retrieve the next page.' + ), + }) + ), handler: async (ctx, input) => { try { const baseUrl = getBaseUrl(ctx); @@ -253,16 +264,18 @@ export const AzureBlob: ConnectorSpec = { isTool: true, description: 'Download the full content of a blob from Azure Blob Storage, returned as base64. Always call getBlobProperties first to check contentLength — do not call this if the blob exceeds 1048576 bytes (1 MB).', - input: z.object({ - container: z - .string() - .describe('The name of the container that holds the blob. Example: "my-container"'), - blobName: z - .string() - .describe( - 'The full name (path) of the blob to download. Example: "logs/2024/january.log"' - ), - }), + input: lazySchema(() => + z.object({ + container: z + .string() + .describe('The name of the container that holds the blob. Example: "my-container"'), + blobName: z + .string() + .describe( + 'The full name (path) of the blob to download. Example: "logs/2024/january.log"' + ), + }) + ), handler: async (ctx, input) => { try { const baseUrl = getBaseUrl(ctx); @@ -287,16 +300,18 @@ export const AzureBlob: ConnectorSpec = { isTool: true, description: 'Get metadata for a blob (content type, size, last modified, etag) without downloading its content. Call this before getBlob to check whether the blob is small enough to download (limit: 1048576 bytes / 1 MB).', - input: z.object({ - container: z - .string() - .describe('The name of the container that holds the blob. Example: "my-container"'), - blobName: z - .string() - .describe( - 'The full name (path) of the blob to inspect. Example: "logs/2024/january.log"' - ), - }), + input: lazySchema(() => + z.object({ + container: z + .string() + .describe('The name of the container that holds the blob. Example: "my-container"'), + blobName: z + .string() + .describe( + 'The full name (path) of the blob to inspect. Example: "logs/2024/january.log"' + ), + }) + ), handler: async (ctx, input) => { try { const baseUrl = getBaseUrl(ctx); diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/brave_search/brave_search.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/brave_search/brave_search.ts index b5f35797c3fd9..fc0552e427f2b 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/brave_search/brave_search.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/brave_search/brave_search.ts @@ -16,7 +16,7 @@ * - Support for localization and safe search */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { i18n } from '@kbn/i18n'; import type { ConnectorSpec } from '../../connector_spec'; @@ -42,29 +42,33 @@ export const BraveSearchConnector: ConnectorSpec = { actions: { webSearch: { isTool: true, - input: z.object({ - q: z.string().describe('Search query'), - count: z - .number() - .int() - .min(1) - .max(20) - .optional() - .default(DEFAULT_COUNT) - .describe('Number of results to return (max 20)'), - offset: z - .number() - .int() - .min(0) - .optional() - .default(DEFAULT_OFFSET) - .describe('Result offset for pagination'), - }), - output: z.object({ - query: z.any().describe('Original query information'), - results: z.array(z.any()).describe('Array of search results'), - type: z.string().describe('Search type'), - }), + input: lazySchema(() => + z.object({ + q: z.string().describe('Search query'), + count: z + .number() + .int() + .min(1) + .max(20) + .optional() + .default(DEFAULT_COUNT) + .describe('Number of results to return (max 20)'), + offset: z + .number() + .int() + .min(0) + .optional() + .default(DEFAULT_OFFSET) + .describe('Result offset for pagination'), + }) + ), + output: lazySchema(() => + z.object({ + query: z.any().describe('Original query information'), + results: z.array(z.any()).describe('Array of search results'), + type: z.string().describe('Search type'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { q: string; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/figma/figma.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/figma/figma.ts index 1455b2edbafe5..f7b3b2be530b7 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/figma/figma.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/figma/figma.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; import type * as Figma from './types'; const FIGMA_API_BASE = 'https://api.figma.com'; @@ -67,21 +67,23 @@ export const FigmaConnector: ConnectorSpec = { 'only pages, depth=2 returns pages and top-level objects). Optionally pass nodeIds to ' + 'retrieve only specific nodes and their subtrees. The response includes the document tree, ' + 'a components map, and a styles map.', - input: z.object({ - fileKey: z - .string() - .describe('File key from the Figma file URL (e.g. from figma.com/file/FILE_KEY/...)'), - nodeIds: z - .string() - .optional() - .describe('Comma-separated node IDs to retrieve specific nodes (e.g. "1:2,1:3")'), - depth: z - .number() - .optional() - .describe( - 'Tree depth: 1 = pages only, 2 = pages + top-level objects; omit for full tree' - ), - }), + input: lazySchema(() => + z.object({ + fileKey: z + .string() + .describe('File key from the Figma file URL (e.g. from figma.com/file/FILE_KEY/...)'), + nodeIds: z + .string() + .optional() + .describe('Comma-separated node IDs to retrieve specific nodes (e.g. "1:2,1:3")'), + depth: z + .number() + .optional() + .describe( + 'Tree depth: 1 = pages only, 2 = pages + top-level objects; omit for full tree' + ), + }) + ), handler: async (ctx, input: Figma.GetFileInput) => { const params: Record = {}; if (input.depth !== undefined) { @@ -112,24 +114,26 @@ export const FigmaConnector: ConnectorSpec = { 'temporary image URLs (valid for 30 days). Supports PNG, JPG, SVG, and PDF formats. ' + 'Use scale (0.01 to 4) to control resolution. Node IDs can be found in Figma URLs ' + '(?node-id=1:2) or from the getFile action output.', - input: z.object({ - fileKey: z.string().describe('File key from the Figma file URL'), - nodeIds: z - .string() - .describe( - 'Comma-separated node IDs to render (e.g. "1:2,1:3"); find in URL ?node-id= or get_file output' - ), - format: z - .enum(['png', 'jpg', 'svg', 'pdf']) - .default('png') - .describe('Image format (default: png)'), - scale: z - .number() - .min(0.01) - .max(4) - .default(1) - .describe('Scale factor between 0.01 and 4 (default: 1)'), - }), + input: lazySchema(() => + z.object({ + fileKey: z.string().describe('File key from the Figma file URL'), + nodeIds: z + .string() + .describe( + 'Comma-separated node IDs to render (e.g. "1:2,1:3"); find in URL ?node-id= or get_file output' + ), + format: z + .enum(['png', 'jpg', 'svg', 'pdf']) + .default('png') + .describe('Image format (default: png)'), + scale: z + .number() + .min(0.01) + .max(4) + .default(1) + .describe('Scale factor between 0.01 and 4 (default: 1)'), + }) + ), handler: async (ctx, input: Figma.RenderNodesInput) => { const params: Record = { ids: input.nodeIds }; if (input.format) { @@ -153,11 +157,13 @@ export const FigmaConnector: ConnectorSpec = { 'List all files in a Figma project. Returns file names, keys, thumbnail URLs, and ' + 'last modified dates. Use the file keys from the results with the getFile or ' + 'renderNodes actions to inspect individual files.', - input: z.object({ - projectId: z - .string() - .describe('Figma project ID (from list with type teamProjects or project URL)'), - }), + input: lazySchema(() => + z.object({ + projectId: z + .string() + .describe('Figma project ID (from list with type teamProjects or project URL)'), + }) + ), handler: async (ctx, input: Figma.ListProjectFilesInput) => { const response = await ctx.client.get( `${FIGMA_API_BASE}/v1/projects/${input.projectId}/files`, @@ -176,20 +182,22 @@ export const FigmaConnector: ConnectorSpec = { 'to browse files. Provide either teamId (from the team page URL, e.g. figma.com/team/123/Team-Name) ' + 'or a full Figma team page url from which the team ID will be extracted. If neither is ' + 'available in the conversation context, ask the user to provide one.', - input: z.object({ - teamId: z - .string() - .optional() - .describe( - 'Figma team ID from the team page URL. If you do not have it, use url instead or ask the user to paste the team page URL (e.g. figma.com/team/123/Team-Name).' - ), - url: z - .string() - .optional() - .describe( - 'Figma team page URL. Provide this if teamId is not available; the team ID will be extracted. If neither teamId nor url is provided, ask the user to paste the team page URL.' - ), - }), + input: lazySchema(() => + z.object({ + teamId: z + .string() + .optional() + .describe( + 'Figma team ID from the team page URL. If you do not have it, use url instead or ask the user to paste the team page URL (e.g. figma.com/team/123/Team-Name).' + ), + url: z + .string() + .optional() + .describe( + 'Figma team page URL. Provide this if teamId is not available; the team ID will be extracted. If neither teamId nor url is provided, ask the user to paste the team page URL.' + ), + }) + ), handler: async (ctx, input: Figma.ListTeamProjectsInput) => { let teamId = input.teamId; if (teamId === undefined && input.url !== undefined) { @@ -219,7 +227,7 @@ export const FigmaConnector: ConnectorSpec = { 'Get the currently authenticated Figma user. Returns the user ID, handle, email, ' + 'and profile image URL for the API credentials in use. Useful for verifying which ' + 'Figma account is connected.', - input: z.object({}), + input: lazySchema(() => z.object({})), handler: async (ctx): Promise => { const response = await ctx.client.get(`${FIGMA_API_BASE}/v1/me`); const data = response.data as Figma.WhoAmIResult; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/firecrawl/firecrawl.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/firecrawl/firecrawl.ts index 925716f8b1c34..3814bf14c31ee 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/firecrawl/firecrawl.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/firecrawl/firecrawl.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; import { ScrapeInputSchema, @@ -93,7 +93,7 @@ export const FirecrawlConnector: ConnectorSpec = { types: ['bearer'], }, - schema: z.object({}), + schema: lazySchema(() => z.object({})), actions: { scrape: { diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/firecrawl/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/firecrawl/types.ts index eedef0b9d698e..ce09e6198f1ef 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/firecrawl/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/firecrawl/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Action input schemas & inferred types @@ -16,155 +16,173 @@ import { z } from '@kbn/zod/v4'; const MAX_MARKDOWN_LENGTH_DESCRIBE = 'Maximum characters of markdown to return. Default 100000; max 500000 to avoid context overflow. Only set a lower value if you already got truncated output and need to fit within a smaller context.'; -export const ScrapeInputSchema = z.object({ - url: z.string().url().describe('The URL of the webpage to scrape. e.g. https://example.com/page'), - onlyMainContent: z - .boolean() - .optional() - .default(true) - .describe( - 'If true (default), extract only the main content of the page, stripping navigation and boilerplate. Set to false to include more of the page.' - ), - waitFor: z - .number() - .int() - .min(0) - .optional() - .default(0) - .describe( - 'Milliseconds to wait before scraping, to allow JavaScript-rendered content to load. e.g. 2000 to wait 2 seconds. Default 0.' - ), - maxMarkdownLength: z - .number() - .int() - .min(1000) - .max(500_000) - .optional() - .default(100_000) - .describe(MAX_MARKDOWN_LENGTH_DESCRIBE), -}); +export const ScrapeInputSchema = lazySchema(() => + z.object({ + url: z + .string() + .url() + .describe('The URL of the webpage to scrape. e.g. https://example.com/page'), + onlyMainContent: z + .boolean() + .optional() + .default(true) + .describe( + 'If true (default), extract only the main content of the page, stripping navigation and boilerplate. Set to false to include more of the page.' + ), + waitFor: z + .number() + .int() + .min(0) + .optional() + .default(0) + .describe( + 'Milliseconds to wait before scraping, to allow JavaScript-rendered content to load. e.g. 2000 to wait 2 seconds. Default 0.' + ), + maxMarkdownLength: z + .number() + .int() + .min(1000) + .max(500_000) + .optional() + .default(100_000) + .describe(MAX_MARKDOWN_LENGTH_DESCRIBE), + }) +); export type ScrapeInput = z.infer; -export const SearchInputSchema = z.object({ - query: z.string().min(1).describe('Search query string. e.g. "elasticsearch query DSL tutorial"'), - limit: z - .number() - .int() - .min(1) - .max(100) - .optional() - .default(5) - .describe('Maximum number of search results to return (1–100). Default 5.'), -}); +export const SearchInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .min(1) + .describe('Search query string. e.g. "elasticsearch query DSL tutorial"'), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .default(5) + .describe('Maximum number of search results to return (1–100). Default 5.'), + }) +); export type SearchInput = z.infer; -export const MapInputSchema = z.object({ - url: z.string().url().describe('Base URL of the website to map. e.g. https://example.com'), - search: z - .string() - .optional() - .describe( - 'Optional keyword to filter discovered URLs. Only URLs matching this term will be returned. e.g. "blog" to find blog pages.' - ), - limit: z - .number() - .int() - .min(1) - .max(100_000) - .optional() - .default(5000) - .describe('Maximum number of URLs to return (1–100000). Default 5000.'), - includeSubdomains: z - .boolean() - .optional() - .default(true) - .describe( - 'If true (default), include URLs from subdomains of the base URL. e.g. docs.example.com when mapping example.com.' - ), -}); +export const MapInputSchema = lazySchema(() => + z.object({ + url: z.string().url().describe('Base URL of the website to map. e.g. https://example.com'), + search: z + .string() + .optional() + .describe( + 'Optional keyword to filter discovered URLs. Only URLs matching this term will be returned. e.g. "blog" to find blog pages.' + ), + limit: z + .number() + .int() + .min(1) + .max(100_000) + .optional() + .default(5000) + .describe('Maximum number of URLs to return (1–100000). Default 5000.'), + includeSubdomains: z + .boolean() + .optional() + .default(true) + .describe( + 'If true (default), include URLs from subdomains of the base URL. e.g. docs.example.com when mapping example.com.' + ), + }) +); export type MapInput = z.infer; -export const CrawlInputSchema = z.object({ - url: z.string().url().describe('Base URL to start crawling from. e.g. https://example.com'), - limit: z - .number() - .int() - .min(1) - .max(100) - .optional() - .default(20) - .describe( - 'Maximum number of pages to crawl (1–100). Default 20. Keep this low (e.g. 5–20) to avoid context overflow.' - ), - maxDiscoveryDepth: z - .number() - .int() - .min(0) - .optional() - .describe( - 'Maximum link depth to follow from the starting URL. 0 = only the start page, 1 = start page and directly linked pages, etc. Omit to use the Firecrawl default.' - ), - allowExternalLinks: z - .boolean() - .optional() - .default(false) - .describe( - 'If true, follow links to other domains during the crawl. Default false (only crawl the starting domain).' - ), -}); +export const CrawlInputSchema = lazySchema(() => + z.object({ + url: z.string().url().describe('Base URL to start crawling from. e.g. https://example.com'), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .default(20) + .describe( + 'Maximum number of pages to crawl (1–100). Default 20. Keep this low (e.g. 5–20) to avoid context overflow.' + ), + maxDiscoveryDepth: z + .number() + .int() + .min(0) + .optional() + .describe( + 'Maximum link depth to follow from the starting URL. 0 = only the start page, 1 = start page and directly linked pages, etc. Omit to use the Firecrawl default.' + ), + allowExternalLinks: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, follow links to other domains during the crawl. Default false (only crawl the starting domain).' + ), + }) +); export type CrawlInput = z.infer; -export const CrawlAndWaitInputSchema = z.object({ - url: z.string().url().describe('Base URL to start crawling from. e.g. https://example.com'), - limit: z - .number() - .int() - .min(1) - .max(100) - .optional() - .default(20) - .describe( - 'Maximum number of pages to crawl (1–100). Default 20. Keep this low (e.g. 5–20) to avoid context overflow.' - ), - maxDiscoveryDepth: z - .number() - .int() - .min(0) - .optional() - .describe( - 'Maximum link depth to follow from the starting URL. 0 = only the start page, 1 = start page and directly linked pages, etc. Omit to use the Firecrawl default.' - ), - allowExternalLinks: z - .boolean() - .optional() - .default(false) - .describe( - 'If true, follow links to other domains during the crawl. Default false (only crawl the starting domain).' - ), - pollIntervalMs: z - .number() - .int() - .min(1000) - .max(60_000) - .optional() - .default(3000) - .describe( - 'How often to poll the crawl job for completion, in milliseconds (1000–60000). Default 3000 (3 seconds).' - ), - maxWaitMs: z - .number() - .int() - .min(5000) - .max(3_600_000) - .optional() - .default(1_800_000) - .describe( - 'Maximum time to wait for the crawl to finish, in milliseconds (5000–3600000). Default 1800000 (30 minutes). If the crawl is still running after this duration, an error is thrown.' - ), -}); +export const CrawlAndWaitInputSchema = lazySchema(() => + z.object({ + url: z.string().url().describe('Base URL to start crawling from. e.g. https://example.com'), + limit: z + .number() + .int() + .min(1) + .max(100) + .optional() + .default(20) + .describe( + 'Maximum number of pages to crawl (1–100). Default 20. Keep this low (e.g. 5–20) to avoid context overflow.' + ), + maxDiscoveryDepth: z + .number() + .int() + .min(0) + .optional() + .describe( + 'Maximum link depth to follow from the starting URL. 0 = only the start page, 1 = start page and directly linked pages, etc. Omit to use the Firecrawl default.' + ), + allowExternalLinks: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, follow links to other domains during the crawl. Default false (only crawl the starting domain).' + ), + pollIntervalMs: z + .number() + .int() + .min(1000) + .max(60_000) + .optional() + .default(3000) + .describe( + 'How often to poll the crawl job for completion, in milliseconds (1000–60000). Default 3000 (3 seconds).' + ), + maxWaitMs: z + .number() + .int() + .min(5000) + .max(3_600_000) + .optional() + .default(1_800_000) + .describe( + 'Maximum time to wait for the crawl to finish, in milliseconds (5000–3600000). Default 1800000 (30 minutes). If the crawl is still running after this duration, an error is thrown.' + ), + }) +); export type CrawlAndWaitInput = z.infer; -export const GetCrawlStatusInputSchema = z.object({ - id: z.string().uuid().describe('Crawl job ID (UUID) from the crawl action'), -}); +export const GetCrawlStatusInputSchema = lazySchema(() => + z.object({ + id: z.string().uuid().describe('Crawl job ID (UUID) from the crawl action'), + }) +); export type GetCrawlStatusInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/gcp_cloud_functions/gcp_cloud_functions.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/gcp_cloud_functions/gcp_cloud_functions.ts index 7a2c1e34a4d4c..e549977c44079 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/gcp_cloud_functions/gcp_cloud_functions.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/gcp_cloud_functions/gcp_cloud_functions.ts @@ -24,7 +24,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ActionContext, ConnectorSpec } from '../../connector_spec'; import { getGcpIdToken, parseServiceAccountKey } from '../../auth_types/gcp_jwt_helpers'; @@ -122,46 +122,50 @@ export const GcpCloudFunctionsConnector: ConnectorSpec = { }, }, - schema: z.object({ - projectId: z - .string() - .min(1) - .describe( - i18n.translate('connectorSpecs.gcpCloudFunctions.config.projectId', { - defaultMessage: 'GCP Project ID', - }) - ) - .meta({ - widget: 'text', - label: i18n.translate('connectorSpecs.gcpCloudFunctions.config.projectId.label', { - defaultMessage: 'GCP Project ID', + schema: lazySchema(() => + z.object({ + projectId: z + .string() + .min(1) + .describe( + i18n.translate('connectorSpecs.gcpCloudFunctions.config.projectId', { + defaultMessage: 'GCP Project ID', + }) + ) + .meta({ + widget: 'text', + label: i18n.translate('connectorSpecs.gcpCloudFunctions.config.projectId.label', { + defaultMessage: 'GCP Project ID', + }), + placeholder: 'my-gcp-project', }), - placeholder: 'my-gcp-project', - }), - region: z - .string() - .min(1) - .describe( - i18n.translate('connectorSpecs.gcpCloudFunctions.config.region', { - defaultMessage: 'GCP Region (e.g., us-central1, europe-west1)', - }) - ) - .meta({ - widget: 'text', - label: i18n.translate('connectorSpecs.gcpCloudFunctions.config.region.label', { - defaultMessage: 'GCP Region', + region: z + .string() + .min(1) + .describe( + i18n.translate('connectorSpecs.gcpCloudFunctions.config.region', { + defaultMessage: 'GCP Region (e.g., us-central1, europe-west1)', + }) + ) + .meta({ + widget: 'text', + label: i18n.translate('connectorSpecs.gcpCloudFunctions.config.region.label', { + defaultMessage: 'GCP Region', + }), + placeholder: 'us-central1', }), - placeholder: 'us-central1', - }), - }), + }) + ), actions: { invoke: { isTool: true, - input: z.object({ - functionName: z.string().min(1).describe('Cloud Function or Cloud Run service name'), - payload: z.unknown().optional().describe('JSON payload to send to the function'), - }), + input: lazySchema(() => + z.object({ + functionName: z.string().min(1).describe('Cloud Function or Cloud Run service name'), + payload: z.unknown().optional().describe('JSON payload to send to the function'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { functionName: string; @@ -206,16 +210,18 @@ export const GcpCloudFunctionsConnector: ConnectorSpec = { listFunctions: { isTool: true, - input: z.object({ - pageSize: z - .number() - .int() - .min(1) - .max(500) - .optional() - .describe('Maximum number of functions to return (1-500)'), - pageToken: z.string().optional().describe('Pagination token from a previous response'), - }), + input: lazySchema(() => + z.object({ + pageSize: z + .number() + .int() + .min(1) + .max(500) + .optional() + .describe('Maximum number of functions to return (1-500)'), + pageToken: z.string().optional().describe('Pagination token from a previous response'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { pageSize?: number; @@ -270,9 +276,11 @@ export const GcpCloudFunctionsConnector: ConnectorSpec = { getFunction: { isTool: true, - input: z.object({ - functionName: z.string().min(1).describe('Cloud Function or Cloud Run service name'), - }), + input: lazySchema(() => + z.object({ + functionName: z.string().min(1).describe('Cloud Function or Cloud Run service name'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { functionName: string; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/github/github.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/github/github.ts index 069acf6393577..fbca2cc8df6ae 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/github/github.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/github/github.ts @@ -16,7 +16,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { UISchemas, type ConnectorSpec } from '../../connector_spec'; import { withMcpClient, callToolContent, callToolJson } from '../../lib/mcp'; import type { @@ -94,21 +94,23 @@ export const GithubConnector: ConnectorSpec = { }, }, - schema: z.object({ - serverUrl: UISchemas.url() - .default(GITHUB_MCP_SERVER_URL) - .describe('GitHub MCP Server URL') - .meta({ - widget: 'text', - placeholder: 'https://api.githubcopilot.com/mcp/', - label: i18n.translate('connectorSpecs.github.config.serverUrl.label', { - defaultMessage: 'MCP Server URL', + schema: lazySchema(() => + z.object({ + serverUrl: UISchemas.url() + .default(GITHUB_MCP_SERVER_URL) + .describe('GitHub MCP Server URL') + .meta({ + widget: 'text', + placeholder: 'https://api.githubcopilot.com/mcp/', + label: i18n.translate('connectorSpecs.github.config.serverUrl.label', { + defaultMessage: 'MCP Server URL', + }), + helpText: i18n.translate('connectorSpecs.github.config.serverUrl.helpText', { + defaultMessage: 'The URL of the GitHub Copilot MCP server.', + }), }), - helpText: i18n.translate('connectorSpecs.github.config.serverUrl.helpText', { - defaultMessage: 'The URL of the GitHub Copilot MCP server.', - }), - }), - }), + }) + ), validateUrls: { fields: ['serverUrl'], diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/github/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/github/types.ts index 296f9c49ca968..bd44dc60973c4 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/github/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/github/types.ts @@ -7,163 +7,217 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Action input schemas & inferred types // ============================================================================= -export const GetMeInputSchema = z.object({}); +export const GetMeInputSchema = lazySchema(() => z.object({})); export type GetMeInput = z.infer; -export const ListToolsInputSchema = z.object({}); +export const ListToolsInputSchema = lazySchema(() => z.object({})); export type ListToolsInput = z.infer; -export const SearchCodeInputSchema = z.object({ - query: z.string().min(1).describe('GitHub code search query'), - page: z.number().optional().default(1), - perPage: z.number().optional().default(10), -}); +export const SearchCodeInputSchema = lazySchema(() => + z.object({ + query: z.string().min(1).describe('GitHub code search query'), + page: z.number().optional().default(1), + perPage: z.number().optional().default(10), + }) +); export type SearchCodeInput = z.infer; -export const SearchRepositoriesInputSchema = z.object({ - query: z.string().min(1).describe('GitHub repository search query'), - page: z.number().optional().default(1), - perPage: z.number().optional().default(10), -}); +export const SearchRepositoriesInputSchema = lazySchema(() => + z.object({ + query: z.string().min(1).describe('GitHub repository search query'), + page: z.number().optional().default(1), + perPage: z.number().optional().default(10), + }) +); export type SearchRepositoriesInput = z.infer; -export const SearchIssuesInputSchema = z.object({ - query: z.string().min(1).describe('GitHub issue search query'), - order: z.enum(['asc', 'desc']).optional().default('desc'), - sort: z.string().optional().default('created'), - page: z.number().optional().default(1), - perPage: z.number().optional().default(10), -}); +export const SearchIssuesInputSchema = lazySchema(() => + z.object({ + query: z.string().min(1).describe('GitHub issue search query'), + order: z.enum(['asc', 'desc']).optional().default('desc'), + sort: z.string().optional().default('created'), + page: z.number().optional().default(1), + perPage: z.number().optional().default(10), + }) +); export type SearchIssuesInput = z.infer; -export const SearchPullRequestsInputSchema = z.object({ - query: z.string().min(1).describe('GitHub pull request search query'), - order: z.enum(['asc', 'desc']).optional().default('desc'), - sort: z.string().optional().default('created'), - page: z.number().optional().default(1), - perPage: z.number().optional().default(10), -}); +export const SearchPullRequestsInputSchema = lazySchema(() => + z.object({ + query: z.string().min(1).describe('GitHub pull request search query'), + order: z.enum(['asc', 'desc']).optional().default('desc'), + sort: z.string().optional().default('created'), + page: z.number().optional().default(1), + perPage: z.number().optional().default(10), + }) +); export type SearchPullRequestsInput = z.infer; -export const SearchUsersInputSchema = z.object({ - query: z.string().min(1).describe('GitHub user search query'), - page: z.number().optional().default(1), - perPage: z.number().optional().default(10), -}); +export const SearchUsersInputSchema = lazySchema(() => + z.object({ + query: z.string().min(1).describe('GitHub user search query'), + page: z.number().optional().default(1), + perPage: z.number().optional().default(10), + }) +); export type SearchUsersInput = z.infer; -export const ListIssuesInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - state: z.enum(['open', 'closed', 'all']).optional().default('open'), - first: z.number().optional().default(10).describe('Number of results to return'), - after: z.string().optional().describe('Cursor for pagination (endCursor from previous response)'), -}); +export const ListIssuesInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + state: z.enum(['open', 'closed', 'all']).optional().default('open'), + first: z.number().optional().default(10).describe('Number of results to return'), + after: z + .string() + .optional() + .describe('Cursor for pagination (endCursor from previous response)'), + }) +); export type ListIssuesInput = z.infer; -export const ListPullRequestsInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - state: z.enum(['open', 'closed', 'all']).optional().default('open'), - first: z.number().optional().default(10).describe('Number of results to return'), - after: z.string().optional().describe('Cursor for pagination (endCursor from previous response)'), -}); +export const ListPullRequestsInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + state: z.enum(['open', 'closed', 'all']).optional().default('open'), + first: z.number().optional().default(10).describe('Number of results to return'), + after: z + .string() + .optional() + .describe('Cursor for pagination (endCursor from previous response)'), + }) +); export type ListPullRequestsInput = z.infer; -export const ListCommitsInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - sha: z.string().optional().describe('Branch name or commit SHA to start listing from'), - first: z.number().optional().default(10).describe('Number of results to return'), - after: z.string().optional().describe('Cursor for pagination (endCursor from previous response)'), -}); +export const ListCommitsInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + sha: z.string().optional().describe('Branch name or commit SHA to start listing from'), + first: z.number().optional().default(10).describe('Number of results to return'), + after: z + .string() + .optional() + .describe('Cursor for pagination (endCursor from previous response)'), + }) +); export type ListCommitsInput = z.infer; -export const ListBranchesInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - first: z.number().optional().default(10).describe('Number of results to return'), - after: z.string().optional().describe('Cursor for pagination (endCursor from previous response)'), -}); +export const ListBranchesInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + first: z.number().optional().default(10).describe('Number of results to return'), + after: z + .string() + .optional() + .describe('Cursor for pagination (endCursor from previous response)'), + }) +); export type ListBranchesInput = z.infer; -export const ListReleasesInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - first: z.number().optional().default(10).describe('Number of results to return'), - after: z.string().optional().describe('Cursor for pagination (endCursor from previous response)'), -}); +export const ListReleasesInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + first: z.number().optional().default(10).describe('Number of results to return'), + after: z + .string() + .optional() + .describe('Cursor for pagination (endCursor from previous response)'), + }) +); export type ListReleasesInput = z.infer; -export const ListTagsInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - first: z.number().optional().default(10).describe('Number of results to return'), - after: z.string().optional().describe('Cursor for pagination (endCursor from previous response)'), -}); +export const ListTagsInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + first: z.number().optional().default(10).describe('Number of results to return'), + after: z + .string() + .optional() + .describe('Cursor for pagination (endCursor from previous response)'), + }) +); export type ListTagsInput = z.infer; -export const GetCommitInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - sha: z.string().min(1).describe('Commit SHA'), -}); +export const GetCommitInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + sha: z.string().min(1).describe('Commit SHA'), + }) +); export type GetCommitInput = z.infer; -export const GetLatestReleaseInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), -}); +export const GetLatestReleaseInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + }) +); export type GetLatestReleaseInput = z.infer; -export const PullRequestReadInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - pullNumber: z.number().describe('Pull request number'), - method: z - .enum(['get', 'get_diff', 'get_review_comments']) - .optional() - .default('get') - .describe('What to retrieve: full PR details, unified diff, or review comments'), -}); +export const PullRequestReadInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + pullNumber: z.number().describe('Pull request number'), + method: z + .enum(['get', 'get_diff', 'get_review_comments']) + .optional() + .default('get') + .describe('What to retrieve: full PR details, unified diff, or review comments'), + }) +); export type PullRequestReadInput = z.infer; -export const GetFileContentsInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - path: z.string().min(1).describe('File or directory path within the repository'), - ref: z - .string() - .optional() - .describe('Branch name, tag, or commit SHA (defaults to default branch)'), -}); +export const GetFileContentsInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + path: z.string().min(1).describe('File or directory path within the repository'), + ref: z + .string() + .optional() + .describe('Branch name, tag, or commit SHA (defaults to default branch)'), + }) +); export type GetFileContentsInput = z.infer; -export const GetIssueInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - issueNumber: z.number().describe('Issue number'), -}); +export const GetIssueInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + issueNumber: z.number().describe('Issue number'), + }) +); export type GetIssueInput = z.infer; -export const GetIssueCommentsInputSchema = z.object({ - owner: z.string().min(1).describe('Repository owner (user or org)'), - repo: z.string().min(1).describe('Repository name'), - issueNumber: z.number().describe('Issue number'), -}); +export const GetIssueCommentsInputSchema = lazySchema(() => + z.object({ + owner: z.string().min(1).describe('Repository owner (user or org)'), + repo: z.string().min(1).describe('Repository name'), + issueNumber: z.number().describe('Issue number'), + }) +); export type GetIssueCommentsInput = z.infer; -export const CallToolInputSchema = z.object({ - name: z.string().min(1).describe('Name of the MCP tool to call'), - arguments: z - .record(z.string(), z.unknown()) - .optional() - .describe('Arguments to pass to the tool (tool-specific)'), -}); +export const CallToolInputSchema = lazySchema(() => + z.object({ + name: z.string().min(1).describe('Name of the MCP tool to call'), + arguments: z + .record(z.string(), z.unknown()) + .optional() + .describe('Arguments to pass to the tool (tool-specific)'), + }) +); export type CallToolInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/gmail/gmail.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/gmail/gmail.ts index 46242feea19fb..b13763fc4f029 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/gmail/gmail.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/gmail/gmail.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'; const DEFAULT_MAX_RESULTS = 10; @@ -77,22 +77,27 @@ export const GmailConnector: ConnectorSpec = { isTool: true, description: 'Search for emails in Gmail. Use a specific query (from:, subject:, is:unread, after:, newer_than:Nd) and limit maxResults (e.g. 10-20) to avoid large responses.', - input: z.object({ - query: z - .string() - .optional() - .describe( - 'Gmail search query using Gmail search operators. Supported operators: from:user@example.com (sender), to:user@example.com (recipient), subject:keyword (subject line), is:unread / is:read (read status), has:attachment (emails with attachments), after:YYYY/MM/DD / before:YYYY/MM/DD (absolute date range), newer_than:7d / older_than:30d (relative date — d=days, m=months, y=years), label:LABELNAME (by label). Combine operators freely: "from:alice@example.com is:unread newer_than:7d". Prefer narrow queries to avoid large responses.' - ), - maxResults: z - .number() - .optional() - .default(DEFAULT_MAX_RESULTS) - .describe( - 'Maximum number of message IDs to return (1-100). Prefer 10-20 to keep context small; increase only if user explicitly needs more.' - ), - pageToken: z.string().optional().describe('Token for pagination from a previous response'), - }), + input: lazySchema(() => + z.object({ + query: z + .string() + .optional() + .describe( + 'Gmail search query using Gmail search operators. Supported operators: from:user@example.com (sender), to:user@example.com (recipient), subject:keyword (subject line), is:unread / is:read (read status), has:attachment (emails with attachments), after:YYYY/MM/DD / before:YYYY/MM/DD (absolute date range), newer_than:7d / older_than:30d (relative date — d=days, m=months, y=years), label:LABELNAME (by label). Combine operators freely: "from:alice@example.com is:unread newer_than:7d". Prefer narrow queries to avoid large responses.' + ), + maxResults: z + .number() + .optional() + .default(DEFAULT_MAX_RESULTS) + .describe( + 'Maximum number of message IDs to return (1-100). Prefer 10-20 to keep context small; increase only if user explicitly needs more.' + ), + pageToken: z + .string() + .optional() + .describe('Token for pagination from a previous response'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { query?: string; @@ -121,21 +126,23 @@ export const GmailConnector: ConnectorSpec = { isTool: true, description: 'Retrieve one Gmail message by ID. You must call searchMessages or listMessages first to get message IDs, then pass one of those IDs here.', - input: z.object({ - messageId: z - .string() - .min(1, { message: 'messageId is required to retrieve a Gmail message' }) - .describe( - 'Required. The Gmail message ID (e.g. from searchMessages or listMessages). Always pass this when calling getMessage.' - ), - format: z - .enum(['minimal', 'full', 'raw']) - .optional() - .default('minimal') - .describe( - 'Message format: use "minimal" (headers only) to save context; use "full" only when the user needs the email body content.' - ), - }), + input: lazySchema(() => + z.object({ + messageId: z + .string() + .min(1, { message: 'messageId is required to retrieve a Gmail message' }) + .describe( + 'Required. The Gmail message ID (e.g. from searchMessages or listMessages). Always pass this when calling getMessage.' + ), + format: z + .enum(['minimal', 'full', 'raw']) + .optional() + .default('minimal') + .describe( + 'Message format: use "minimal" (headers only) to save context; use "full" only when the user needs the email body content.' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { messageId: string; format?: string }; try { @@ -156,20 +163,22 @@ export const GmailConnector: ConnectorSpec = { isTool: true, description: 'Retrieve one Gmail attachment by message ID and attachment ID. Call getMessage with format "full" first to get attachment IDs from payload.parts[].body.attachmentId (and parts[].filename for the file name).', - input: z.object({ - messageId: z - .string() - .min(1, { message: 'messageId is required to retrieve an attachment' }) - .describe( - 'Required. The Gmail message ID (from getMessage or search/list). Get attachment IDs from getMessage with format "full" — see payload.parts[].body.attachmentId.' - ), - attachmentId: z - .string() - .min(1, { message: 'attachmentId is required to retrieve an attachment' }) - .describe( - 'Required. The attachment ID from the message. Call getMessage with format "full" and read payload.parts[].body.attachmentId (and parts[].filename for the file name).' - ), - }), + input: lazySchema(() => + z.object({ + messageId: z + .string() + .min(1, { message: 'messageId is required to retrieve an attachment' }) + .describe( + 'Required. The Gmail message ID (from getMessage or search/list). Get attachment IDs from getMessage with format "full" — see payload.parts[].body.attachmentId.' + ), + attachmentId: z + .string() + .min(1, { message: 'attachmentId is required to retrieve an attachment' }) + .describe( + 'Required. The attachment ID from the message. Call getMessage with format "full" and read payload.parts[].body.attachmentId (and parts[].filename for the file name).' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { messageId: string; attachmentId: string }; try { @@ -187,22 +196,27 @@ export const GmailConnector: ConnectorSpec = { isTool: true, description: 'List Gmail message IDs by label (e.g. INBOX, SENT). Prefer searchMessages when the user has a specific query; limit maxResults (e.g. 10-20) to keep context small.', - input: z.object({ - maxResults: z - .number() - .optional() - .default(DEFAULT_MAX_RESULTS) - .describe( - 'Maximum number of message IDs to return (1-100). Prefer 10-20 to keep context small.' - ), - pageToken: z.string().optional().describe('Token for pagination from a previous response'), - labelIds: z - .array(z.string()) - .optional() - .describe( - 'Filter messages by Gmail label IDs (e.g. ["INBOX"], ["SENT"], ["UNREAD"]). Use this to scope to a mailbox folder. Omit to list from all labels. Prefer searchMessages when you need query-based filtering.' - ), - }), + input: lazySchema(() => + z.object({ + maxResults: z + .number() + .optional() + .default(DEFAULT_MAX_RESULTS) + .describe( + 'Maximum number of message IDs to return (1-100). Prefer 10-20 to keep context small.' + ), + pageToken: z + .string() + .optional() + .describe('Token for pagination from a previous response'), + labelIds: z + .array(z.string()) + .optional() + .describe( + 'Filter messages by Gmail label IDs (e.g. ["INBOX"], ["SENT"], ["UNREAD"]). Use this to scope to a mailbox folder. Omit to list from all labels. Prefer searchMessages when you need query-based filtering.' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { maxResults?: number; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_calendar/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_calendar/types.ts index bef92b4cd4410..9608c1ff82565 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_calendar/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_calendar/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Shared types @@ -17,136 +17,146 @@ import { z } from '@kbn/zod/v4'; // Action input schemas & inferred types // ============================================================================= -export const SearchEventsInputSchema = z.object({ - query: z - .string() - .min(1) - .describe( - 'Free text search terms to find events that match in summary, description, location, ' + - "attendee names, or other fields. Examples: 'team standup', 'budget review', 'John Smith'." - ), - calendarId: z - .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) - .default('primary') - .describe( - "Calendar ID to search. Use 'primary' for the user's primary calendar, a specific calendar ID from listCalendars, or a person's email address to access their calendar." - ), - timeMin: z - .string() - .optional() - .describe( - 'Lower bound (inclusive) for event start time, as an RFC3339 timestamp. Example: 2024-01-01T00:00:00Z' - ), - timeMax: z - .string() - .optional() - .describe( - 'Upper bound (exclusive) for event start time, as an RFC3339 timestamp. Example: 2024-12-31T23:59:59Z' - ), - maxResults: z - .number() - .min(1) - .max(2500) - .default(50) - .describe('Maximum number of events to return (1-2500, default 50)'), - orderBy: z - .preprocess( - (val) => (val === '' ? undefined : val), - z.enum(['startTime', 'updated']).optional() - ) - .describe( - "Sort order: 'startTime' (chronological, default) or 'updated' (last modification time)" - ), -}); +export const SearchEventsInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .min(1) + .describe( + 'Free text search terms to find events that match in summary, description, location, ' + + "attendee names, or other fields. Examples: 'team standup', 'budget review', 'John Smith'." + ), + calendarId: z + .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) + .default('primary') + .describe( + "Calendar ID to search. Use 'primary' for the user's primary calendar, a specific calendar ID from listCalendars, or a person's email address to access their calendar." + ), + timeMin: z + .string() + .optional() + .describe( + 'Lower bound (inclusive) for event start time, as an RFC3339 timestamp. Example: 2024-01-01T00:00:00Z' + ), + timeMax: z + .string() + .optional() + .describe( + 'Upper bound (exclusive) for event start time, as an RFC3339 timestamp. Example: 2024-12-31T23:59:59Z' + ), + maxResults: z + .number() + .min(1) + .max(2500) + .default(50) + .describe('Maximum number of events to return (1-2500, default 50)'), + orderBy: z + .preprocess( + (val) => (val === '' ? undefined : val), + z.enum(['startTime', 'updated']).optional() + ) + .describe( + "Sort order: 'startTime' (chronological, default) or 'updated' (last modification time)" + ), + }) +); export type SearchEventsInput = z.infer; -export const GetEventInputSchema = z.object({ - eventId: z - .string() - .min(1) - .describe('The ID of the event to retrieve. Use event IDs from search or list results.'), - calendarId: z - .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) - .default('primary') - .describe( - "Calendar ID containing the event. Use 'primary' for the user's primary calendar, or a person's email address to access their calendar." - ), -}); +export const GetEventInputSchema = lazySchema(() => + z.object({ + eventId: z + .string() + .min(1) + .describe('The ID of the event to retrieve. Use event IDs from search or list results.'), + calendarId: z + .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) + .default('primary') + .describe( + "Calendar ID containing the event. Use 'primary' for the user's primary calendar, or a person's email address to access their calendar." + ), + }) +); export type GetEventInput = z.infer; -export const ListCalendarsInputSchema = z.object({ - pageToken: z - .string() - .optional() - .describe( - "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page." - ), -}); +export const ListCalendarsInputSchema = lazySchema(() => + z.object({ + pageToken: z + .string() + .optional() + .describe( + "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page." + ), + }) +); export type ListCalendarsInput = z.infer; -export const ListEventsInputSchema = z.object({ - calendarId: z - .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) - .default('primary') - .describe( - "Calendar ID to list events from. Use 'primary' for the user's primary calendar, a specific calendar ID from listCalendars, or a person's email address to access their calendar." - ), - timeMin: z - .string() - .optional() - .describe( - 'Lower bound (inclusive) for event start time, as an RFC3339 timestamp. Example: 2024-01-01T00:00:00Z' - ), - timeMax: z - .string() - .optional() - .describe( - 'Upper bound (exclusive) for event start time, as an RFC3339 timestamp. Example: 2024-12-31T23:59:59Z' - ), - maxResults: z - .number() - .min(1) - .max(2500) - .default(50) - .describe('Maximum number of events to return (1-2500, default 50)'), - pageToken: z - .string() - .optional() - .describe( - "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page." - ), - orderBy: z - .preprocess( - (val) => (val === '' ? undefined : val), - z.enum(['startTime', 'updated']).optional() - ) - .describe( - "Sort order: 'startTime' (chronological, requires singleEvents=true) or 'updated' (last modification time)" - ), -}); +export const ListEventsInputSchema = lazySchema(() => + z.object({ + calendarId: z + .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) + .default('primary') + .describe( + "Calendar ID to list events from. Use 'primary' for the user's primary calendar, a specific calendar ID from listCalendars, or a person's email address to access their calendar." + ), + timeMin: z + .string() + .optional() + .describe( + 'Lower bound (inclusive) for event start time, as an RFC3339 timestamp. Example: 2024-01-01T00:00:00Z' + ), + timeMax: z + .string() + .optional() + .describe( + 'Upper bound (exclusive) for event start time, as an RFC3339 timestamp. Example: 2024-12-31T23:59:59Z' + ), + maxResults: z + .number() + .min(1) + .max(2500) + .default(50) + .describe('Maximum number of events to return (1-2500, default 50)'), + pageToken: z + .string() + .optional() + .describe( + "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page." + ), + orderBy: z + .preprocess( + (val) => (val === '' ? undefined : val), + z.enum(['startTime', 'updated']).optional() + ) + .describe( + "Sort order: 'startTime' (chronological, requires singleEvents=true) or 'updated' (last modification time)" + ), + }) +); export type ListEventsInput = z.infer; -export const FreeBusyInputSchema = z.object({ - timeMin: z - .string() - .min(1) - .describe( - 'Start of the time interval to query, as an RFC3339 timestamp. Example: 2024-01-15T09:00:00Z' - ), - timeMax: z - .string() - .min(1) - .describe( - 'End of the time interval to query, as an RFC3339 timestamp. Example: 2024-01-15T18:00:00Z' - ), - calendarIds: z - .array(z.string().min(1)) - .min(1) - .describe( - "List of calendar IDs to check availability for. Use 'primary' for the user's own calendar, or a person's email address to check their availability. Example: ['primary', 'colleague@company.com']" - ), - timeZone: z - .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) - .describe('Time zone for the query (optional, defaults to UTC). Example: America/New_York'), -}); +export const FreeBusyInputSchema = lazySchema(() => + z.object({ + timeMin: z + .string() + .min(1) + .describe( + 'Start of the time interval to query, as an RFC3339 timestamp. Example: 2024-01-15T09:00:00Z' + ), + timeMax: z + .string() + .min(1) + .describe( + 'End of the time interval to query, as an RFC3339 timestamp. Example: 2024-01-15T18:00:00Z' + ), + calendarIds: z + .array(z.string().min(1)) + .min(1) + .describe( + "List of calendar IDs to check availability for. Use 'primary' for the user's own calendar, or a person's email address to check their availability. Example: ['primary', 'colleague@company.com']" + ), + timeZone: z + .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) + .describe('Time zone for the query (optional, defaults to UTC). Example: America/New_York'), + }) +); export type FreeBusyInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_cloud_storage/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_cloud_storage/types.ts index e0b30ca0b3cd0..02d057473871c 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_cloud_storage/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_cloud_storage/types.ts @@ -7,95 +7,105 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; const DEFAULT_PAGE_SIZE = 100; -export const ListProjectsInputSchema = z.object({ - pageSize: z - .number() - .optional() - .describe(`Maximum number of projects to return (default: ${DEFAULT_PAGE_SIZE}, max 1000)`), - pageToken: z - .string() - .optional() - .describe('Pagination token from a previous response to get the next page of results'), - filter: z - .string() - .optional() - .describe( - 'Optional filter expression. Supported operators: "name:" (project name contains), "id:" (exact project ID), "lifecycleState:" (ACTIVE, DELETE_REQUESTED, etc.). Examples: "name:production", "lifecycleState:ACTIVE".' - ), -}); +export const ListProjectsInputSchema = lazySchema(() => + z.object({ + pageSize: z + .number() + .optional() + .describe(`Maximum number of projects to return (default: ${DEFAULT_PAGE_SIZE}, max 1000)`), + pageToken: z + .string() + .optional() + .describe('Pagination token from a previous response to get the next page of results'), + filter: z + .string() + .optional() + .describe( + 'Optional filter expression. Supported operators: "name:" (project name contains), "id:" (exact project ID), "lifecycleState:" (ACTIVE, DELETE_REQUESTED, etc.). Examples: "name:production", "lifecycleState:ACTIVE".' + ), + }) +); export type ListProjectsInput = z.infer; -export const ListBucketsInputSchema = z.object({ - project: z - .string() - .min(1) - .describe( - 'Google Cloud project ID (e.g. "my-project-123"). Use listProjects to discover available project IDs.' - ), - maxResults: z - .number() - .optional() - .describe(`Maximum number of buckets to return (default: ${DEFAULT_PAGE_SIZE}, max 1000)`), - pageToken: z - .string() - .optional() - .describe('Pagination token from a previous response to get the next page of results'), - prefix: z.string().optional().describe('Filter buckets whose names begin with this prefix'), -}); +export const ListBucketsInputSchema = lazySchema(() => + z.object({ + project: z + .string() + .min(1) + .describe( + 'Google Cloud project ID (e.g. "my-project-123"). Use listProjects to discover available project IDs.' + ), + maxResults: z + .number() + .optional() + .describe(`Maximum number of buckets to return (default: ${DEFAULT_PAGE_SIZE}, max 1000)`), + pageToken: z + .string() + .optional() + .describe('Pagination token from a previous response to get the next page of results'), + prefix: z.string().optional().describe('Filter buckets whose names begin with this prefix'), + }) +); export type ListBucketsInput = z.infer; -export const ListObjectsInputSchema = z.object({ - bucket: z.string().min(1).describe('Name of the GCS bucket to list objects from'), - prefix: z - .string() - .optional() - .describe( - 'Filter objects whose names begin with this prefix. Use to navigate "folders" (e.g. "reports/2024/")' - ), - delimiter: z - .string() - .optional() - .describe( - 'Character used to group object names. Use "/" to list only the current folder level' - ), - maxResults: z - .number() - .optional() - .describe(`Maximum number of objects to return (default: ${DEFAULT_PAGE_SIZE}, max 1000)`), - pageToken: z - .string() - .optional() - .describe('Pagination token from a previous response to get the next page of results'), -}); +export const ListObjectsInputSchema = lazySchema(() => + z.object({ + bucket: z.string().min(1).describe('Name of the GCS bucket to list objects from'), + prefix: z + .string() + .optional() + .describe( + 'Filter objects whose names begin with this prefix. Use to navigate "folders" (e.g. "reports/2024/")' + ), + delimiter: z + .string() + .optional() + .describe( + 'Character used to group object names. Use "/" to list only the current folder level' + ), + maxResults: z + .number() + .optional() + .describe(`Maximum number of objects to return (default: ${DEFAULT_PAGE_SIZE}, max 1000)`), + pageToken: z + .string() + .optional() + .describe('Pagination token from a previous response to get the next page of results'), + }) +); export type ListObjectsInput = z.infer; -export const GetObjectMetadataInputSchema = z.object({ - bucket: z.string().min(1).describe('Name of the GCS bucket containing the object'), - object: z - .string() - .min(1) - .describe('Full name/path of the object (e.g. "reports/2024/january.pdf")'), -}); +export const GetObjectMetadataInputSchema = lazySchema(() => + z.object({ + bucket: z.string().min(1).describe('Name of the GCS bucket containing the object'), + object: z + .string() + .min(1) + .describe('Full name/path of the object (e.g. "reports/2024/january.pdf")'), + }) +); export type GetObjectMetadataInput = z.infer; const DEFAULT_MAX_DOWNLOAD_SIZE_BYTES = 768000; // ~750 KB (safe ceiling before base64 hits the 1 MB platform response limit) -export const DownloadObjectInputSchema = z.object({ - bucket: z.string().min(1).describe('Name of the GCS bucket containing the object'), - object: z - .string() - .min(1) - .describe('Full name/path of the object to download (e.g. "reports/jan.pdf", "data/q1.csv")'), - maximumDownloadSizeBytes: z - .number() - .optional() - .default(DEFAULT_MAX_DOWNLOAD_SIZE_BYTES) - .describe( - `Maximum file size in bytes to download inline. Files exceeding this limit return metadata only. Default is ${DEFAULT_MAX_DOWNLOAD_SIZE_BYTES} (~750 KB).` - ), -}); +export const DownloadObjectInputSchema = lazySchema(() => + z.object({ + bucket: z.string().min(1).describe('Name of the GCS bucket containing the object'), + object: z + .string() + .min(1) + .describe('Full name/path of the object to download (e.g. "reports/jan.pdf", "data/q1.csv")'), + maximumDownloadSizeBytes: z + .number() + .optional() + .default(DEFAULT_MAX_DOWNLOAD_SIZE_BYTES) + .describe( + `Maximum file size in bytes to download inline. Files exceeding this limit return metadata only. Default is ${DEFAULT_MAX_DOWNLOAD_SIZE_BYTES} (~750 KB).` + ), + }) +); export type DownloadObjectInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.ts index c4b64937fceab..f0ffb2688d14f 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/google_drive/google_drive.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; // Google Drive API constants const GOOGLE_DRIVE_API_BASE = 'https://www.googleapis.com/drive/v3'; @@ -96,53 +96,55 @@ export const GoogleDriveConnector: ConnectorSpec = { isTool: true, description: "Search for files in Google Drive using Google's query syntax. Use this to find files by name, content, type, owner, or modification date across the entire Drive.", - input: z.object({ - query: z - .string() - .min(1) - .describe( - 'Google Drive search query passed verbatim to the Drive API `q` parameter. ' + - 'Key patterns: ' + - "name match: name contains 'budget' | " + - "full-text search: fullText contains 'quarterly report' | " + - "MIME type filter: mimeType = 'application/pdf' | " + - "owner filter: 'me' in owners | " + - "date filter: modifiedTime > '2024-01-01' | " + - "folder contents: '' in parents | " + - 'exclude trash: trashed = false (always add unless the user asks for trashed files). ' + - "Operators: contains, =, !=, <, >, <=, >=. Combine clauses with 'and' / 'or'. " + - 'String values must use single quotes. ' + - "Example: name contains 'budget' and mimeType = 'application/pdf' and trashed = false" - ), - pageSize: z - .number() - .max(1000) - .default(DEFAULT_PAGE_SIZE) - .describe('Number of results to return (default 250, max 1000)'), - pageToken: z - .string() - .optional() - .describe( - "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page. When nextPageToken is absent in the response, there are no more results." - ), - orderBy: z - .preprocess( - (val) => (val === '' ? undefined : val), - z - .enum([ - 'createdTime', - 'createdTime desc', - 'modifiedTime', - 'modifiedTime desc', - 'name', - 'name desc', - ]) - .optional() - ) - .describe( - "Sort order for results. Options: 'createdTime', 'createdTime desc', 'modifiedTime', 'modifiedTime desc', 'name', or 'name desc'" - ), - }), + input: lazySchema(() => + z.object({ + query: z + .string() + .min(1) + .describe( + 'Google Drive search query passed verbatim to the Drive API `q` parameter. ' + + 'Key patterns: ' + + "name match: name contains 'budget' | " + + "full-text search: fullText contains 'quarterly report' | " + + "MIME type filter: mimeType = 'application/pdf' | " + + "owner filter: 'me' in owners | " + + "date filter: modifiedTime > '2024-01-01' | " + + "folder contents: '' in parents | " + + 'exclude trash: trashed = false (always add unless the user asks for trashed files). ' + + "Operators: contains, =, !=, <, >, <=, >=. Combine clauses with 'and' / 'or'. " + + 'String values must use single quotes. ' + + "Example: name contains 'budget' and mimeType = 'application/pdf' and trashed = false" + ), + pageSize: z + .number() + .max(1000) + .default(DEFAULT_PAGE_SIZE) + .describe('Number of results to return (default 250, max 1000)'), + pageToken: z + .string() + .optional() + .describe( + "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page. When nextPageToken is absent in the response, there are no more results." + ), + orderBy: z + .preprocess( + (val) => (val === '' ? undefined : val), + z + .enum([ + 'createdTime', + 'createdTime desc', + 'modifiedTime', + 'modifiedTime desc', + 'name', + 'name desc', + ]) + .optional() + ) + .describe( + "Sort order for results. Options: 'createdTime', 'createdTime desc', 'modifiedTime', 'modifiedTime desc', 'name', or 'name desc'" + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { query: string; @@ -186,36 +188,38 @@ export const GoogleDriveConnector: ConnectorSpec = { isTool: true, description: 'List files and subfolders within a specific Google Drive folder. Use this to browse folder contents by folder ID, or start at the root folder.', - input: z.object({ - folderId: z - .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) - .default(DEFAULT_FOLDER_ID) - .describe( - "Folder ID to list contents of. Use 'root' for the root folder, or a folder ID from search/list results. Defaults to 'root'." - ), - pageSize: z - .number() - .max(1000) - .default(DEFAULT_PAGE_SIZE) - .describe('Number of results to return (default 250, max 1000)'), - pageToken: z - .string() - .optional() - .describe( - "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page. When nextPageToken is absent in the response, there are no more results." - ), - orderBy: z - .preprocess( - (val) => (val === '' ? undefined : val), - z.enum(['name', 'modifiedTime', 'createdTime']).optional() - ) - .describe("Sort order for results. Options: 'name', 'modifiedTime', or 'createdTime'"), - includeTrashed: z - .boolean() - .optional() - .default(false) - .describe('Whether to include trashed files in results (default: false)'), - }), + input: lazySchema(() => + z.object({ + folderId: z + .preprocess((val) => (val === '' ? undefined : val), z.string().optional()) + .default(DEFAULT_FOLDER_ID) + .describe( + "Folder ID to list contents of. Use 'root' for the root folder, or a folder ID from search/list results. Defaults to 'root'." + ), + pageSize: z + .number() + .max(1000) + .default(DEFAULT_PAGE_SIZE) + .describe('Number of results to return (default 250, max 1000)'), + pageToken: z + .string() + .optional() + .describe( + "Pagination token. Pass the 'nextPageToken' value from a previous response to get the next page. When nextPageToken is absent in the response, there are no more results." + ), + orderBy: z + .preprocess( + (val) => (val === '' ? undefined : val), + z.enum(['name', 'modifiedTime', 'createdTime']).optional() + ) + .describe("Sort order for results. Options: 'name', 'modifiedTime', or 'createdTime'"), + includeTrashed: z + .boolean() + .optional() + .default(false) + .describe('Whether to include trashed files in results (default: false)'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { folderId: string; @@ -262,14 +266,16 @@ export const GoogleDriveConnector: ConnectorSpec = { isTool: true, description: 'Download a file from Google Drive and return its content as base64-encoded data. Works with PDFs, Office documents, Google Docs (exported as PDF), Google Sheets (exported as XLSX), and other binary or text-based formats. Use file IDs from searchFiles or listFiles results. WARNING: Returns potentially large base64 payloads. Only call this when you have a plan to process the binary data (e.g. via an Elasticsearch ingest pipeline attachment processor). For text-based files, prefer reading metadata first to confirm the file type.', - input: z.object({ - fileId: z - .string() - .min(1) - .describe( - 'The ID of the file to download. Use IDs from searchFiles or listFiles results. Works with PDFs, Office docs, Google Docs, and other text-based formats.' - ), - }), + input: lazySchema(() => + z.object({ + fileId: z + .string() + .min(1) + .describe( + 'The ID of the file to download. Use IDs from searchFiles or listFiles results. Works with PDFs, Office docs, Google Docs, and other text-based formats.' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { fileId: string; @@ -344,14 +350,16 @@ export const GoogleDriveConnector: ConnectorSpec = { isTool: true, description: 'Get detailed metadata for one or more specific files, including ownership, sharing status, permissions, labels, and descriptions. Use after searchFiles or listFiles to inspect specific files in depth.', - input: z.object({ - fileIds: z - .array(z.string().min(1)) - .min(1) - .describe( - 'Array of file IDs to fetch metadata for. Use IDs from searchFiles or listFiles results. Returns ownership, sharing, permissions, and other details for each file.' - ), - }), + input: lazySchema(() => + z.object({ + fileIds: z + .array(z.string().min(1)) + .min(1) + .describe( + 'Array of file IDs to fetch metadata for. Use IDs from searchFiles or listFiles results. Returns ownership, sharing, permissions, and other details for each file.' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { fileIds: string[]; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/greynoise/greynoise.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/greynoise/greynoise.ts index a996cfb28576e..3dd7cb5350f77 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/greynoise/greynoise.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/greynoise/greynoise.ts @@ -19,10 +19,16 @@ * MVP implementation focusing on core noise detection actions. */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { i18n } from '@kbn/i18n'; import type { ConnectorSpec } from '../../connector_spec'; +const IpInputSchema = lazySchema(() => + z.object({ + ip: z.ipv4().describe('IP address'), + }) +); + export const GreyNoiseConnector: ConnectorSpec = { metadata: { id: '.greynoise', @@ -41,9 +47,7 @@ export const GreyNoiseConnector: ConnectorSpec = { actions: { getIpContext: { isTool: true, - input: z.object({ - ip: z.ipv4().describe('IP address'), - }), + input: IpInputSchema, handler: async (ctx, input) => { const typedInput = input as { ip: string }; const response = await ctx.client.get( @@ -63,9 +67,7 @@ export const GreyNoiseConnector: ConnectorSpec = { quickLookup: { isTool: true, - input: z.object({ - ip: z.ipv4().describe('IP address'), - }), + input: IpInputSchema, handler: async (ctx, input) => { const typedInput = input as { ip: string }; const response = await ctx.client.get( @@ -82,9 +84,7 @@ export const GreyNoiseConnector: ConnectorSpec = { getMetadata: { isTool: true, - input: z.object({ - ip: z.ipv4().describe('IP address'), - }), + input: IpInputSchema, handler: async (ctx, input) => { const typedInput = input as { ip: string }; const response = await ctx.client.get('https://api.greynoise.io/v2/meta/metadata', { @@ -104,9 +104,7 @@ export const GreyNoiseConnector: ConnectorSpec = { riotLookup: { isTool: true, - input: z.object({ - ip: z.ipv4().describe('IP address'), - }), + input: IpInputSchema, handler: async (ctx, input) => { const typedInput = input as { ip: string }; const response = await ctx.client.get(`https://api.greynoise.io/v2/riot/${typedInput.ip}`); diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/jina/jina_reader.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/jina/jina_reader.ts index 443a99da01e65..d9d9001d110cf 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/jina/jina_reader.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/jina/jina_reader.ts @@ -13,7 +13,7 @@ * MVP implementation focusing on core reader features. */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { i18n } from '@kbn/i18n'; import { UISchemas, type ConnectorSpec } from '../../connector_spec'; @@ -81,46 +81,50 @@ export const JinaReaderConnector: ConnectorSpec = { ], }, - schema: z.object({ - overrideBrowseUrl: UISchemas.url() - .optional() - .default(JINA_READER_BROWSE_URL) - .describe('Override Jina Reader Browse URL') - .meta({ - widget: 'text', - label: 'Browse URL', - placeholder: JINA_READER_BROWSE_URL, - }), - overrideSearchUrl: UISchemas.url() - .optional() - .default(JINA_READER_SEARCH_URL) - .describe('Override Jina Reader Search URL') - .meta({ - widget: 'text', - label: 'Search URL', - placeholder: JINA_READER_SEARCH_URL, - }), - }), + schema: lazySchema(() => + z.object({ + overrideBrowseUrl: UISchemas.url() + .optional() + .default(JINA_READER_BROWSE_URL) + .describe('Override Jina Reader Browse URL') + .meta({ + widget: 'text', + label: 'Browse URL', + placeholder: JINA_READER_BROWSE_URL, + }), + overrideSearchUrl: UISchemas.url() + .optional() + .default(JINA_READER_SEARCH_URL) + .describe('Override Jina Reader Search URL') + .meta({ + widget: 'text', + label: 'Search URL', + placeholder: JINA_READER_SEARCH_URL, + }), + }) + ), actions: { browse: { isTool: true, description: 'Turn any URL to markdown for LLM consumption', - input: z.object({ - url: z.string().min(3).describe('URL to browse'), - returnFormat: z - .enum([ - RETURN_FORMAT.MARKDOWN, - RETURN_FORMAT.FULL_MARKDOWN, - RETURN_FORMAT.PLAIN_TEXT, - RETURN_FORMAT.SCREENSHOT, - RETURN_FORMAT.FULL_SCREENSHOT, - RETURN_FORMAT.HTML, - ]) - .optional() - .describe('Desired return format'), - options: z.record(z.string(), z.any()).optional().describe('Additional advanced options'), - }), + input: lazySchema(() => + z.object({ + url: z.string().min(3).describe('URL to browse'), + returnFormat: z + .enum([ + RETURN_FORMAT.MARKDOWN, + RETURN_FORMAT.FULL_MARKDOWN, + RETURN_FORMAT.PLAIN_TEXT, + RETURN_FORMAT.SCREENSHOT, + RETURN_FORMAT.FULL_SCREENSHOT, + RETURN_FORMAT.HTML, + ]) + .optional() + .describe('Desired return format'), + options: z.record(z.string(), z.any()).optional().describe('Additional advanced options'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { url: string; @@ -153,14 +157,16 @@ export const JinaReaderConnector: ConnectorSpec = { search: { isTool: true, description: 'Web search to find relevant context for LLMs', - input: z.object({ - query: z.string().min(1).describe('Search query'), - returnFormat: z - .enum([RETURN_FORMAT.MARKDOWN, RETURN_FORMAT.FULL_MARKDOWN, RETURN_FORMAT.PLAIN_TEXT]) - .optional() - .describe('Desired return format'), - options: z.record(z.string(), z.any()).optional().describe('Additional advanced options'), - }), + input: lazySchema(() => + z.object({ + query: z.string().min(1).describe('Search query'), + returnFormat: z + .enum([RETURN_FORMAT.MARKDOWN, RETURN_FORMAT.FULL_MARKDOWN, RETURN_FORMAT.PLAIN_TEXT]) + .optional() + .describe('Desired return format'), + options: z.record(z.string(), z.any()).optional().describe('Additional advanced options'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { query: string; @@ -195,11 +201,13 @@ export const JinaReaderConnector: ConnectorSpec = { fileToMarkdown: { isTool: true, description: 'Convert a file to markdown for LLM consumption', - input: z.object({ - file: z.string().describe('Base64-encoded file content'), - filename: z.string().optional().describe('Original filename'), - options: z.record(z.string(), z.any()).optional().describe('Additional advanced options'), - }), + input: lazySchema(() => + z.object({ + file: z.string().describe('Base64-encoded file content'), + filename: z.string().optional().describe('Original filename'), + options: z.record(z.string(), z.any()).optional().describe('Additional advanced options'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { file: string; @@ -234,12 +242,14 @@ export const JinaReaderConnector: ConnectorSpec = { fileToRenderedImage: { isTool: true, description: 'Render a document file to image. Office and PDF files supported.', - input: z.object({ - file: z.string().describe('Base64-encoded file content'), - filename: z.string().optional().describe('Original filename'), - pageNumber: z.number().optional().describe('Page number to render (starting from 1)'), - options: z.record(z.string(), z.any()).optional().describe('Additional advanced options'), - }), + input: lazySchema(() => + z.object({ + file: z.string().describe('Base64-encoded file content'), + filename: z.string().optional().describe('Original filename'), + pageNumber: z.number().optional().describe('Page number to render (starting from 1)'), + options: z.record(z.string(), z.any()).optional().describe('Additional advanced options'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { file: string; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/microsoft_teams/microsoft_teams.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/microsoft_teams/microsoft_teams.ts index 4681ced697b29..d16ee66d1a826 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/microsoft_teams/microsoft_teams.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/microsoft_teams/microsoft_teams.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; import { ListJoinedTeamsInputSchema, @@ -33,10 +33,12 @@ import type { */ const userPath = (userId?: string): string => (userId ? `/users/${userId}` : '/me'); -const GraphCollectionOutputSchema = z.object({ - value: z.array(z.any()).describe('Array of items returned from the API'), - '@odata.nextLink': z.string().optional().describe('URL to fetch next page of results'), -}); +const GraphCollectionOutputSchema = lazySchema(() => + z.object({ + value: z.array(z.any()).describe('Array of items returned from the API'), + '@odata.nextLink': z.string().optional().describe('URL to fetch next page of results'), + }) +); export const MicrosoftTeams: ConnectorSpec = { metadata: { @@ -281,19 +283,21 @@ export const MicrosoftTeams: ConnectorSpec = { description: 'Search Teams messages using the Microsoft Graph Search API. Requires delegated authentication (bearer token or OAuth authorization code). Not supported with app-only (client credentials) auth — Microsoft does not allow application permissions for chatMessage search.', input: SearchMessagesInputSchema, - output: z - .object({ - value: z - .array( - z.object({ - hitsContainers: z - .array(z.any()) - .describe('Containers with search hits and associated metadata'), - }) - ) - .describe('Search response containers'), - }) - .describe('Microsoft Graph Search API response'), + output: lazySchema(() => + z + .object({ + value: z + .array( + z.object({ + hitsContainers: z + .array(z.any()) + .describe('Containers with search hits and associated metadata'), + }) + ) + .describe('Search response containers'), + }) + .describe('Microsoft Graph Search API response') + ), handler: async (ctx, input: SearchMessagesInput) => { if (ctx.secrets?.authType === 'oauth_client_credentials') { throw new Error( diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/microsoft_teams/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/microsoft_teams/types.ts index 707d2ae01e56b..73a0b4031b448 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/microsoft_teams/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/microsoft_teams/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Action input schemas & inferred types @@ -25,83 +25,95 @@ export const ListJoinedTeamsInputSchema = z .optional(); export type ListJoinedTeamsInput = z.infer; -export const ListChannelsInputSchema = z.object({ - teamId: z - .string() - .describe( - 'The ID of the Microsoft Team whose channels you want to list. Obtain this from listJoinedTeams (the "id" field on each team object).' - ), -}); +export const ListChannelsInputSchema = lazySchema(() => + z.object({ + teamId: z + .string() + .describe( + 'The ID of the Microsoft Team whose channels you want to list. Obtain this from listJoinedTeams (the "id" field on each team object).' + ), + }) +); export type ListChannelsInput = z.infer; -export const ListChannelMessagesInputSchema = z.object({ - teamId: z - .string() - .describe( - 'The ID of the Microsoft Team containing the channel. Obtain this from listJoinedTeams (the "id" field on each team object).' - ), - channelId: z - .string() - .describe( - 'The ID of the channel whose messages you want to retrieve. Obtain this from listChannels (the "id" field on each channel object).' - ), - top: z - .number() - .min(1) - .max(50) - .default(20) - .describe('Number of messages to return (max 50; default: 20)'), -}); +export const ListChannelMessagesInputSchema = lazySchema(() => + z.object({ + teamId: z + .string() + .describe( + 'The ID of the Microsoft Team containing the channel. Obtain this from listJoinedTeams (the "id" field on each team object).' + ), + channelId: z + .string() + .describe( + 'The ID of the channel whose messages you want to retrieve. Obtain this from listChannels (the "id" field on each channel object).' + ), + top: z + .number() + .min(1) + .max(50) + .default(20) + .describe('Number of messages to return (max 50; default: 20)'), + }) +); export type ListChannelMessagesInput = z.infer; -export const ListChatsInputSchema = z.object({ - userId: z - .string() - .optional() - .describe( - 'User ID for app-only auth via client credentials. Omit when using delegated auth (bearer token).' - ), - top: z - .number() - .min(1) - .max(50) - .default(20) - .describe('Number of chats to return (max 50; default: 20)'), -}); +export const ListChatsInputSchema = lazySchema(() => + z.object({ + userId: z + .string() + .optional() + .describe( + 'User ID for app-only auth via client credentials. Omit when using delegated auth (bearer token).' + ), + top: z + .number() + .min(1) + .max(50) + .default(20) + .describe('Number of chats to return (max 50; default: 20)'), + }) +); export type ListChatsInput = z.infer; -export const ListChatMessagesInputSchema = z.object({ - chatId: z - .string() - .describe( - 'The ID of the chat (direct message or group chat) whose messages you want to retrieve. Obtain this from listChats (the "id" field on each chat object).' - ), - top: z - .number() - .min(1) - .max(50) - .default(20) - .describe('Number of messages to return (max 50; default: 20)'), -}); +export const ListChatMessagesInputSchema = lazySchema(() => + z.object({ + chatId: z + .string() + .describe( + 'The ID of the chat (direct message or group chat) whose messages you want to retrieve. Obtain this from listChats (the "id" field on each chat object).' + ), + top: z + .number() + .min(1) + .max(50) + .default(20) + .describe('Number of messages to return (max 50; default: 20)'), + }) +); export type ListChatMessagesInput = z.infer; -export const SearchMessagesInputSchema = z.object({ - query: z.string().describe('Search query (supports KQL syntax, e.g. "from:bob sent>2024-01-01")'), - from: z - .number() - .optional() - .describe( - 'Zero-based offset for pagination (default: 0). Combine with size to page through results.' - ), - size: z - .number() - .min(1) - .max(25) - .default(25) - .describe('Number of results to return (max 25; default: 25 when omitted)'), - enableTopResults: z - .boolean() - .default(false) - .describe('Sort results by relevance (default: false)'), -}); +export const SearchMessagesInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .describe('Search query (supports KQL syntax, e.g. "from:bob sent>2024-01-01")'), + from: z + .number() + .optional() + .describe( + 'Zero-based offset for pagination (default: 0). Combine with size to page through results.' + ), + size: z + .number() + .min(1) + .max(25) + .default(25) + .describe('Number of results to return (max 25; default: 25 when omitted)'), + enableTopResults: z + .boolean() + .default(false) + .describe('Sort results by relevance (default: false)'), + }) +); export type SearchMessagesInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/notion/notion.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/notion/notion.ts index 5c09effff2ae1..cd5d9a1d14b3b 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/notion/notion.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/notion/notion.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; import type * as Notion from './types'; export const NotionConnector: ConnectorSpec = { @@ -51,31 +51,33 @@ export const NotionConnector: ConnectorSpec = { isTool: true, description: 'Search for Notion pages or data sources (databases) whose title contains a given string. Use this to discover what pages or databases exist before fetching their content or querying their rows.', - input: z.object({ - query: z - .string() - .describe( - 'The text string to search for within page or data source titles. Example: "Engineering roadmap"' - ), - queryObjectType: z - .enum(['page', 'data_source']) - .describe( - 'Whether to search for pages ("page") or databases/data sources ("data_source"). Use "page" to find wiki-style content, and "data_source" to find structured databases.' - ), - startCursor: z - .string() - .optional() - .describe( - 'Pagination cursor returned from a previous search response. Pass this to retrieve the next page of results.' - ), - pageSize: z - .number() - .max(100) - .default(10) - .describe( - 'Maximum number of results to return per page. Defaults to 10 if not specified.' - ), - }), + input: lazySchema(() => + z.object({ + query: z + .string() + .describe( + 'The text string to search for within page or data source titles. Example: "Engineering roadmap"' + ), + queryObjectType: z + .enum(['page', 'data_source']) + .describe( + 'Whether to search for pages ("page") or databases/data sources ("data_source"). Use "page" to find wiki-style content, and "data_source" to find structured databases.' + ), + startCursor: z + .string() + .optional() + .describe( + 'Pagination cursor returned from a previous search response. Pass this to retrieve the next page of results.' + ), + pageSize: z + .number() + .max(100) + .default(10) + .describe( + 'Maximum number of results to return per page. Defaults to 10 if not specified.' + ), + }) + ), handler: async (ctx, input: Notion.SearchByTitleInput) => { const response = await ctx.client.post('https://api.notion.com/v1/search', { query: input.query, @@ -96,13 +98,15 @@ export const NotionConnector: ConnectorSpec = { isTool: true, description: 'Given the ID of a Notion page, retrieve its metadata — including title, properties, parent, created/edited timestamps, and URL. Use this after finding a page ID via searchPageOrDSByTitle.', - input: z.object({ - pageId: z - .string() - .describe( - 'The Notion page ID to retrieve. This is the UUID found in the page URL or returned by searchPageOrDSByTitle. Example: "5b2c3d4e-1234-5678-abcd-ef0123456789"' - ), - }), + input: lazySchema(() => + z.object({ + pageId: z + .string() + .describe( + 'The Notion page ID to retrieve. This is the UUID found in the page URL or returned by searchPageOrDSByTitle. Example: "5b2c3d4e-1234-5678-abcd-ef0123456789"' + ), + }) + ), handler: async (ctx, input: Notion.GetPageInput) => { const response = await ctx.client.get( `https://api.notion.com/v1/pages/${input.pageId}`, @@ -117,13 +121,15 @@ export const NotionConnector: ConnectorSpec = { isTool: true, description: 'Given the ID of a Notion data source (database), retrieve its schema — including the names, types, and options for all columns/properties. Use this before querying rows so you know what filters and fields are available.', - input: z.object({ - dataSourceId: z - .string() - .describe( - 'The Notion data source (database) ID to inspect. This is the UUID found in the database URL or returned by searchPageOrDSByTitle. Example: "a1b2c3d4-5678-90ab-cdef-1234567890ab"' - ), - }), + input: lazySchema(() => + z.object({ + dataSourceId: z + .string() + .describe( + 'The Notion data source (database) ID to inspect. This is the UUID found in the database URL or returned by searchPageOrDSByTitle. Example: "a1b2c3d4-5678-90ab-cdef-1234567890ab"' + ), + }) + ), handler: async (ctx, input: Notion.GetDataSourceInput) => { const response = await ctx.client.get( `https://api.notion.com/v1/data_sources/${input.dataSourceId}`, @@ -138,39 +144,41 @@ export const NotionConnector: ConnectorSpec = { isTool: true, description: 'Given the ID of a Notion data source (database), query its rows. Returns up to 10 rows by default. Supports filtering via the Notion filter JSON format (see https://developers.notion.com/reference/filter-data-source-entries) and cursor-based pagination. Use getDataSource first to understand the available columns and their types before constructing a filter.', - input: z.object({ - dataSourceId: z - .string() - .describe( - 'The Notion data source (database) ID to query. Example: "a1b2c3d4-5678-90ab-cdef-1234567890ab"' - ), - filter: z - .string() - .optional() - .describe( - 'Optional filter expressed as a JSON string following the Notion filter format (https://developers.notion.com/reference/filter-data-source-entries). Each filter targets one property by name and applies a type-specific condition. Common patterns:\n' + - '- Status: {"property":"Status","status":{"equals":"In progress"}}\n' + - '- Checkbox: {"property":"Done","checkbox":{"equals":true}}\n' + - '- Text contains: {"property":"Name","rich_text":{"contains":"keyword"}}\n' + - '- Date after: {"property":"Due","date":{"after":"2024-01-01"}}\n' + - '- Compound (AND): {"and":[{...},{...}]}\n' + - '- Compound (OR): {"or":[{...},{...}]}\n' + - 'Always call getDataSource first to discover the available property names and their types before constructing a filter. Omit to return all rows up to pageSize.' - ), - startCursor: z - .string() - .optional() - .describe( - 'Pagination cursor returned from a previous queryDataSource response. Pass this to fetch the next page of rows.' - ), - pageSize: z - .number() - .max(100) - .default(10) - .describe( - 'Maximum number of rows to return. Defaults to 10 if not specified. Maximum allowed by Notion is 100.' - ), - }), + input: lazySchema(() => + z.object({ + dataSourceId: z + .string() + .describe( + 'The Notion data source (database) ID to query. Example: "a1b2c3d4-5678-90ab-cdef-1234567890ab"' + ), + filter: z + .string() + .optional() + .describe( + 'Optional filter expressed as a JSON string following the Notion filter format (https://developers.notion.com/reference/filter-data-source-entries). Each filter targets one property by name and applies a type-specific condition. Common patterns:\n' + + '- Status: {"property":"Status","status":{"equals":"In progress"}}\n' + + '- Checkbox: {"property":"Done","checkbox":{"equals":true}}\n' + + '- Text contains: {"property":"Name","rich_text":{"contains":"keyword"}}\n' + + '- Date after: {"property":"Due","date":{"after":"2024-01-01"}}\n' + + '- Compound (AND): {"and":[{...},{...}]}\n' + + '- Compound (OR): {"or":[{...},{...}]}\n' + + 'Always call getDataSource first to discover the available property names and their types before constructing a filter. Omit to return all rows up to pageSize.' + ), + startCursor: z + .string() + .optional() + .describe( + 'Pagination cursor returned from a previous queryDataSource response. Pass this to fetch the next page of rows.' + ), + pageSize: z + .number() + .max(100) + .default(10) + .describe( + 'Maximum number of rows to return. Defaults to 10 if not specified. Maximum allowed by Notion is 100.' + ), + }) + ), handler: async (ctx, input: Notion.QueryDataSourceInput) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let requestData: Record = { diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/one_password/one_password.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/one_password/one_password.ts index 4f2bc34848893..7e5db2dfb81d7 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/one_password/one_password.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/one_password/one_password.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; const BASE_URL = 'https://api.1password.com/v1beta1'; @@ -60,21 +60,23 @@ export const OnePasswordConnector: ConnectorSpec = { 'User-Agent': 'ElasticKibana', }, }, - schema: z.object({ - accountUuid: z - .string() - .min(1, { - message: i18n.translate( - 'core.kibanaConnectorSpecs.onePassword.config.accountUuidRequired', - { defaultMessage: 'Account UUID is required' } - ), - }) - .meta({ - label: i18n.translate('core.kibanaConnectorSpecs.onePassword.config.accountUuid', { - defaultMessage: 'Account UUID', + schema: lazySchema(() => + z.object({ + accountUuid: z + .string() + .min(1, { + message: i18n.translate( + 'core.kibanaConnectorSpecs.onePassword.config.accountUuidRequired', + { defaultMessage: 'Account UUID is required' } + ), + }) + .meta({ + label: i18n.translate('core.kibanaConnectorSpecs.onePassword.config.accountUuid', { + defaultMessage: 'Account UUID', + }), }), - }), - }), + }) + ), actions: { listUsers: { @@ -83,11 +85,13 @@ export const OnePasswordConnector: ConnectorSpec = { 'core.kibanaConnectorSpecs.onePassword.actions.listUsers.description', { defaultMessage: 'List users in the 1Password account, optionally filtered by state' } ), - input: z.object({ - filter: z.enum(['user.isActive()', 'user.isSuspended()']).optional(), - maxPageSize: z.number().optional(), - pageToken: z.string().optional(), - }), + input: lazySchema(() => + z.object({ + filter: z.enum(['user.isActive()', 'user.isSuspended()']).optional(), + maxPageSize: z.number().optional(), + pageToken: z.string().optional(), + }) + ), handler: async (ctx, input) => { const typedInput = input as { filter?: 'user.isActive()' | 'user.isSuspended()'; @@ -117,9 +121,11 @@ export const OnePasswordConnector: ConnectorSpec = { 'core.kibanaConnectorSpecs.onePassword.actions.getUser.description', { defaultMessage: 'Get details for a single user by their UUID' } ), - input: z.object({ - uuid: z.string().min(1), - }), + input: lazySchema(() => + z.object({ + uuid: z.string().min(1), + }) + ), handler: async (ctx, input) => { const { uuid } = input as { uuid: string }; const { accountUuid } = ctx.config as { accountUuid: string }; @@ -144,9 +150,11 @@ export const OnePasswordConnector: ConnectorSpec = { 'Suspend an active user, preventing them from accessing the 1Password account', } ), - input: z.object({ - uuid: z.string().min(1), - }), + input: lazySchema(() => + z.object({ + uuid: z.string().min(1), + }) + ), handler: async (ctx, input) => { const { uuid } = input as { uuid: string }; const { accountUuid } = ctx.config as { accountUuid: string }; @@ -168,9 +176,11 @@ export const OnePasswordConnector: ConnectorSpec = { 'core.kibanaConnectorSpecs.onePassword.actions.reactivateUser.description', { defaultMessage: 'Reactivate a suspended user, restoring their access to 1Password' } ), - input: z.object({ - uuid: z.string().min(1), - }), + input: lazySchema(() => + z.object({ + uuid: z.string().min(1), + }) + ), handler: async (ctx, input) => { const { uuid } = input as { uuid: string }; const { accountUuid } = ctx.config as { accountUuid: string }; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/pagerduty/pagerduty.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/pagerduty/pagerduty.ts index c512ec9845814..b1b33aa4549af 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/pagerduty/pagerduty.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/pagerduty/pagerduty.ts @@ -16,7 +16,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { UISchemas, type ConnectorSpec } from '../../connector_spec'; import { withMcpClient, callToolContent, callToolJson } from '../../lib/mcp'; import type { @@ -86,21 +86,23 @@ export const PagerdutyConnector: ConnectorSpec = { ], }, - schema: z.object({ - serverUrl: UISchemas.url() - .default(PAGERDUTY_MCP_SERVER_URL) - .describe('PagerDuty MCP Server URL') - .meta({ - widget: 'text', - placeholder: 'https://mcp.pagerduty.com/mcp', - label: i18n.translate('connectorSpecs.pagerduty.config.serverUrl.label', { - defaultMessage: 'MCP Server URL', + schema: lazySchema(() => + z.object({ + serverUrl: UISchemas.url() + .default(PAGERDUTY_MCP_SERVER_URL) + .describe('PagerDuty MCP Server URL') + .meta({ + widget: 'text', + placeholder: 'https://mcp.pagerduty.com/mcp', + label: i18n.translate('connectorSpecs.pagerduty.config.serverUrl.label', { + defaultMessage: 'MCP Server URL', + }), + helpText: i18n.translate('connectorSpecs.pagerduty.config.serverUrl.helpText', { + defaultMessage: 'The URL of the PagerDuty MCP server.', + }), }), - helpText: i18n.translate('connectorSpecs.pagerduty.config.serverUrl.helpText', { - defaultMessage: 'The URL of the PagerDuty MCP server.', - }), - }), - }), + }) + ), validateUrls: { fields: ['serverUrl'], diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/pagerduty/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/pagerduty/types.ts index 67a17a1cc00aa..cc39657641d34 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/pagerduty/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/pagerduty/types.ts @@ -7,214 +7,236 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Action input schemas & inferred types // ============================================================================= -export const ListToolsInputSchema = z.object({}); +export const ListToolsInputSchema = lazySchema(() => z.object({})); export type ListToolsInput = z.infer; -export const GetUserDataInputSchema = z.object({}); +export const GetUserDataInputSchema = lazySchema(() => z.object({})); export type GetUserDataInput = z.infer; -export const ListSchedulesInputSchema = z.object({ - query: z - .string() - .optional() - .describe( - 'Free-text search string across name and description fields (e.g., "primary" or "weekend")' - ), - limit: z.number().optional().describe('Maximum number of schedules to return'), - include: z - .array(z.string()) - .optional() - .describe( - 'Related resources to include. Valid values: schedule_layers, overrides_subschedule, final_schedule' - ), - team_ids: z - .array(z.string()) - .optional() - .describe('Filter schedules to those belonging to these team IDs (e.g., ["P123ABC"])'), - user_ids: z - .array(z.string()) - .optional() - .describe('Filter schedules to those containing these user IDs (e.g., ["P456DEF"])'), -}); +export const ListSchedulesInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .optional() + .describe( + 'Free-text search string across name and description fields (e.g., "primary" or "weekend")' + ), + limit: z.number().optional().describe('Maximum number of schedules to return'), + include: z + .array(z.string()) + .optional() + .describe( + 'Related resources to include. Valid values: schedule_layers, overrides_subschedule, final_schedule' + ), + team_ids: z + .array(z.string()) + .optional() + .describe('Filter schedules to those belonging to these team IDs (e.g., ["P123ABC"])'), + user_ids: z + .array(z.string()) + .optional() + .describe('Filter schedules to those containing these user IDs (e.g., ["P456DEF"])'), + }) +); export type ListSchedulesInput = z.infer; -export const ListEscalationPoliciesInputSchema = z.object({ - query: z - .string() - .optional() - .describe( - 'Free-text search string across name and description fields (e.g., "production" or "on-call")' - ), - limit: z.number().optional().describe('Maximum number of escalation policies to return'), - user_ids: z - .array(z.string()) - .optional() - .describe('Filter escalation policies by user IDs (e.g., ["P123ABC"])'), - team_ids: z - .array(z.string()) - .optional() - .describe('Filter escalation policies by team IDs (e.g., ["P456DEF"])'), -}); +export const ListEscalationPoliciesInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .optional() + .describe( + 'Free-text search string across name and description fields (e.g., "production" or "on-call")' + ), + limit: z.number().optional().describe('Maximum number of escalation policies to return'), + user_ids: z + .array(z.string()) + .optional() + .describe('Filter escalation policies by user IDs (e.g., ["P123ABC"])'), + team_ids: z + .array(z.string()) + .optional() + .describe('Filter escalation policies by team IDs (e.g., ["P456DEF"])'), + }) +); export type ListEscalationPoliciesInput = z.infer; -export const ListIncidentsInputSchema = z.object({ - limit: z - .number() - .max(1000) - .default(25) - .describe('Maximum number of incidents to return (max 1000, default 25)'), - status: z - .array(z.string()) - .optional() - .describe( - 'Filter by incident status. Allowed values: triggered, acknowledged, resolved (e.g., ["triggered", "acknowledged"])' - ), - service_ids: z - .array(z.string()) - .optional() - .describe('Filter incidents to those belonging to these service IDs (e.g., ["P123ABC"])'), - user_ids: z - .array(z.string()) - .optional() - .describe( - 'Filter incidents assigned to these user IDs (e.g., ["P456DEF"]). Only used when request_scope is "assigned"' - ), - since: z - .string() - .optional() - .describe('Start of the date range in ISO 8601 format (e.g., "2024-01-01T00:00:00Z")'), - until: z - .string() - .optional() - .describe('End of the date range in ISO 8601 format (e.g., "2024-01-31T23:59:59Z")'), - urgencies: z - .array(z.string()) - .optional() - .describe('Filter by urgency level. Allowed values: high, low (e.g., ["high"])'), - request_scope: z - .enum(['all', 'teams', 'assigned']) - .optional() - .describe( - 'Scope of incidents to return: "all" (default) returns all incidents, "teams" returns team incidents, "assigned" returns incidents assigned to the current user' - ), - sort_by: z - .array(z.string()) - .optional() - .describe( - 'Sort field(s) and direction, max 2 entries. Allowed fields: incident_number, created_at, resolved_at, urgency. Use colon for direction (e.g., "created_at:desc" or "incident_number:asc"). Default direction is asc.' - ), -}); +export const ListIncidentsInputSchema = lazySchema(() => + z.object({ + limit: z + .number() + .max(1000) + .default(25) + .describe('Maximum number of incidents to return (max 1000, default 25)'), + status: z + .array(z.string()) + .optional() + .describe( + 'Filter by incident status. Allowed values: triggered, acknowledged, resolved (e.g., ["triggered", "acknowledged"])' + ), + service_ids: z + .array(z.string()) + .optional() + .describe('Filter incidents to those belonging to these service IDs (e.g., ["P123ABC"])'), + user_ids: z + .array(z.string()) + .optional() + .describe( + 'Filter incidents assigned to these user IDs (e.g., ["P456DEF"]). Only used when request_scope is "assigned"' + ), + since: z + .string() + .optional() + .describe('Start of the date range in ISO 8601 format (e.g., "2024-01-01T00:00:00Z")'), + until: z + .string() + .optional() + .describe('End of the date range in ISO 8601 format (e.g., "2024-01-31T23:59:59Z")'), + urgencies: z + .array(z.string()) + .optional() + .describe('Filter by urgency level. Allowed values: high, low (e.g., ["high"])'), + request_scope: z + .enum(['all', 'teams', 'assigned']) + .optional() + .describe( + 'Scope of incidents to return: "all" (default) returns all incidents, "teams" returns team incidents, "assigned" returns incidents assigned to the current user' + ), + sort_by: z + .array(z.string()) + .optional() + .describe( + 'Sort field(s) and direction, max 2 entries. Allowed fields: incident_number, created_at, resolved_at, urgency. Use colon for direction (e.g., "created_at:desc" or "incident_number:asc"). Default direction is asc.' + ), + }) +); export type ListIncidentsInput = z.infer; -export const ListOncallsInputSchema = z.object({ - limit: z - .number() - .optional() - .default(20) - .describe('Maximum number of on-call results to return (default 20)'), - schedule_ids: z - .array(z.string()) - .optional() - .describe( - 'Filter on-call results to these schedule IDs (e.g., ["P123ABC", "P456DEF"]). Use this to find who is on call for specific schedules.' - ), - user_ids: z - .array(z.string()) - .optional() - .describe('Filter on-call results to these user IDs (e.g., ["P789GHI"])'), - escalation_policy_ids: z - .array(z.string()) - .optional() - .describe( - 'Filter on-call results to these escalation policy IDs (e.g., ["PABCDEF"]). Use this to find who is on call for a specific escalation policy.' - ), - since: z - .string() - .optional() - .describe( - 'Start of the time range for on-call periods in ISO 8601 format (e.g., "2024-01-01T00:00:00Z"). Defaults to current time.' - ), - until: z - .string() - .optional() - .describe( - 'End of the time range for on-call periods in ISO 8601 format (e.g., "2024-01-02T00:00:00Z")' - ), - time_zone: z - .string() - .optional() - .describe( - 'IANA time zone database name to render dates in (e.g., "America/New_York" or "Europe/London")' - ), - earliest: z - .boolean() - .optional() - .describe( - 'If true, return only the earliest on-call entry for each unique user+escalation policy combination. Useful for finding who is currently on call without duplicates. Default is true.' - ), -}); +export const ListOncallsInputSchema = lazySchema(() => + z.object({ + limit: z + .number() + .optional() + .default(20) + .describe('Maximum number of on-call results to return (default 20)'), + schedule_ids: z + .array(z.string()) + .optional() + .describe( + 'Filter on-call results to these schedule IDs (e.g., ["P123ABC", "P456DEF"]). Use this to find who is on call for specific schedules.' + ), + user_ids: z + .array(z.string()) + .optional() + .describe('Filter on-call results to these user IDs (e.g., ["P789GHI"])'), + escalation_policy_ids: z + .array(z.string()) + .optional() + .describe( + 'Filter on-call results to these escalation policy IDs (e.g., ["PABCDEF"]). Use this to find who is on call for a specific escalation policy.' + ), + since: z + .string() + .optional() + .describe( + 'Start of the time range for on-call periods in ISO 8601 format (e.g., "2024-01-01T00:00:00Z"). Defaults to current time.' + ), + until: z + .string() + .optional() + .describe( + 'End of the time range for on-call periods in ISO 8601 format (e.g., "2024-01-02T00:00:00Z")' + ), + time_zone: z + .string() + .optional() + .describe( + 'IANA time zone database name to render dates in (e.g., "America/New_York" or "Europe/London")' + ), + earliest: z + .boolean() + .optional() + .describe( + 'If true, return only the earliest on-call entry for each unique user+escalation policy combination. Useful for finding who is currently on call without duplicates. Default is true.' + ), + }) +); export type ListOncallsInput = z.infer; -export const ListUsersInputSchema = z.object({ - query: z - .string() - .optional() - .describe( - 'Free-text search across name and email fields (e.g., "alice" or "alice@example.com")' - ), - limit: z.number().optional().describe('Maximum number of users to return'), -}); +export const ListUsersInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .optional() + .describe( + 'Free-text search across name and email fields (e.g., "alice" or "alice@example.com")' + ), + limit: z.number().optional().describe('Maximum number of users to return'), + }) +); export type ListUsersInput = z.infer; -export const ListTeamsInputSchema = z.object({ - query: z - .string() - .optional() - .describe('Free-text search across name and description fields (e.g., "platform" or "sre")'), - limit: z.number().optional().describe('Maximum number of teams to return'), -}); +export const ListTeamsInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .optional() + .describe('Free-text search across name and description fields (e.g., "platform" or "sre")'), + limit: z.number().optional().describe('Maximum number of teams to return'), + }) +); export type ListTeamsInput = z.infer; -export const GetScheduleInputSchema = z.object({ - schedule_id: z - .string() - .min(1) - .describe('The PagerDuty schedule ID to retrieve (e.g., "P123ABC")'), -}); +export const GetScheduleInputSchema = lazySchema(() => + z.object({ + schedule_id: z + .string() + .min(1) + .describe('The PagerDuty schedule ID to retrieve (e.g., "P123ABC")'), + }) +); export type GetScheduleInput = z.infer; -export const GetIncidentInputSchema = z.object({ - incident_id: z - .string() - .min(1) - .describe('The PagerDuty incident ID to retrieve (e.g., "Q1A2B3C4D5E6F7")'), -}); +export const GetIncidentInputSchema = lazySchema(() => + z.object({ + incident_id: z + .string() + .min(1) + .describe('The PagerDuty incident ID to retrieve (e.g., "Q1A2B3C4D5E6F7")'), + }) +); export type GetIncidentInput = z.infer; -export const GetEscalationPolicyInputSchema = z.object({ - policy_id: z - .string() - .min(1) - .describe('The PagerDuty escalation policy ID to retrieve (e.g., "P123ABC")'), -}); +export const GetEscalationPolicyInputSchema = lazySchema(() => + z.object({ + policy_id: z + .string() + .min(1) + .describe('The PagerDuty escalation policy ID to retrieve (e.g., "P123ABC")'), + }) +); export type GetEscalationPolicyInput = z.infer; -export const GetTeamInputSchema = z.object({ - team_id: z.string().min(1).describe('The PagerDuty team ID to retrieve (e.g., "P123ABC")'), -}); +export const GetTeamInputSchema = lazySchema(() => + z.object({ + team_id: z.string().min(1).describe('The PagerDuty team ID to retrieve (e.g., "P123ABC")'), + }) +); export type GetTeamInput = z.infer; -export const CallToolInputSchema = z.object({ - name: z.string().min(1).describe('Name of the MCP tool to call'), - arguments: z - .record(z.string(), z.unknown()) - .optional() - .describe('Arguments to pass to the tool (tool-specific)'), -}); +export const CallToolInputSchema = lazySchema(() => + z.object({ + name: z.string().min(1).describe('Name of the MCP tool to call'), + arguments: z + .record(z.string(), z.unknown()) + .optional() + .describe('Arguments to pass to the tool (tool-specific)'), + }) +); export type CallToolInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/salesforce/salesforce.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/salesforce/salesforce.ts index 35e878d4194d8..913d999fc9f56 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/salesforce/salesforce.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/salesforce/salesforce.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; const SALESFORCE_API_VERSION = 'v66.0'; @@ -87,14 +87,16 @@ export const SalesforceConnector: ConnectorSpec = { actions: { query: { isTool: true, - input: z.object({ - soql: z - .string() - .describe( - 'SOQL query. Prefer LIMIT 10-20 and WHERE to narrow results; use nextRecordsUrl from response for more.' - ), - nextRecordsUrl: z.string().optional().describe('Pagination URL from previous response'), - }), + input: lazySchema(() => + z.object({ + soql: z + .string() + .describe( + 'SOQL query. Prefer LIMIT 10-20 and WHERE to narrow results; use nextRecordsUrl from response for more.' + ), + nextRecordsUrl: z.string().optional().describe('Pagination URL from previous response'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { soql: string; nextRecordsUrl?: string }; const baseUrl = getBaseUrl(ctx.secrets?.tokenUrl as string | undefined); @@ -113,16 +115,20 @@ export const SalesforceConnector: ConnectorSpec = { get_record: { isTool: true, - input: z.object({ - sobjectName: z - .string() - .describe( - 'SObject API name (standard or custom, e.g. Account, Contact, MyObject__c). Must match the object that owns the record.' - ), - recordId: z - .string() - .describe('Record Id (15- or 18-char). Get from query, list_records, or search results.'), - }), + input: lazySchema(() => + z.object({ + sobjectName: z + .string() + .describe( + 'SObject API name (standard or custom, e.g. Account, Contact, MyObject__c). Must match the object that owns the record.' + ), + recordId: z + .string() + .describe( + 'Record Id (15- or 18-char). Get from query, list_records, or search results.' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { sobjectName: string; recordId: string }; validateSobjectName(typedInput.sobjectName); @@ -139,14 +145,16 @@ export const SalesforceConnector: ConnectorSpec = { list_records: { isTool: true, - input: z.object({ - sobjectName: z.string().describe('SObject API name (e.g. Account, Contact, MyObject__c)'), - limit: z - .number() - .default(10) - .describe('Max records to return (1-2000). Prefer 10-20 to keep context small.'), - nextRecordsUrl: z.string().optional().describe('Pagination URL from previous response'), - }), + input: lazySchema(() => + z.object({ + sobjectName: z.string().describe('SObject API name (e.g. Account, Contact, MyObject__c)'), + limit: z + .number() + .default(10) + .describe('Max records to return (1-2000). Prefer 10-20 to keep context small.'), + nextRecordsUrl: z.string().optional().describe('Pagination URL from previous response'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { sobjectName: string; @@ -172,19 +180,21 @@ export const SalesforceConnector: ConnectorSpec = { search: { isTool: true, - input: z.object({ - searchTerm: z - .string() - .describe( - 'Search phrase for SOSL full-text search (e.g. "Acme Corp" or "Q4 renewal"). Only searches objects listed in returning; not all text fields are indexed; results capped at ~2000. Prefer query (SOQL) for structured filtering; use search for broad text discovery.' - ), - returning: z - .string() - .describe( - 'Object API names to search, comma-separated (e.g. Account,Contact). Prefer 1-3 types to keep result size down. Custom objects require "Allow Search" enabled. Use describe to discover object names.' - ), - nextRecordsUrl: z.string().optional().describe('Pagination URL from previous response'), - }), + input: lazySchema(() => + z.object({ + searchTerm: z + .string() + .describe( + 'Search phrase for SOSL full-text search (e.g. "Acme Corp" or "Q4 renewal"). Only searches objects listed in returning; not all text fields are indexed; results capped at ~2000. Prefer query (SOQL) for structured filtering; use search for broad text discovery.' + ), + returning: z + .string() + .describe( + 'Object API names to search, comma-separated (e.g. Account,Contact). Prefer 1-3 types to keep result size down. Custom objects require "Allow Search" enabled. Use describe to discover object names.' + ), + nextRecordsUrl: z.string().optional().describe('Pagination URL from previous response'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { searchTerm: string; @@ -210,13 +220,15 @@ export const SalesforceConnector: ConnectorSpec = { describe: { isTool: true, - input: z.object({ - sobjectName: z - .string() - .describe( - 'SObject API name. Use before query or search to discover field names, relationships, and picklist values. Common standard objects you can describe without prior discovery: Account (companies/orgs), Contact (people linked to Account), Opportunity (sales deals with stage/amount/close date), Case (support tickets), Lead (unqualified prospects), Task (action items/follow-ups), ContentVersion (file/attachment versions; use with ContentDocumentLink for downloads). Custom objects always end with __c (e.g. MyObject__c).' - ), - }), + input: lazySchema(() => + z.object({ + sobjectName: z + .string() + .describe( + 'SObject API name. Use before query or search to discover field names, relationships, and picklist values. Common standard objects you can describe without prior discovery: Account (companies/orgs), Contact (people linked to Account), Opportunity (sales deals with stage/amount/close date), Case (support tickets), Lead (unqualified prospects), Task (action items/follow-ups), ContentVersion (file/attachment versions; use with ContentDocumentLink for downloads). Custom objects always end with __c (e.g. MyObject__c).' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { sobjectName: string }; validateSobjectName(typedInput.sobjectName); @@ -234,13 +246,15 @@ export const SalesforceConnector: ConnectorSpec = { isTool: true, description: 'Download a file from Salesforce by its ContentVersion Id. Returns the file as base64-encoded data with its content type. WARNING: Returns potentially large base64 payloads. Only call this when you have a plan to process the binary data (e.g. via an Elasticsearch ingest pipeline attachment processor). Use SOQL on ContentDocumentLink and ContentVersion to discover file Ids first.', - input: z.object({ - contentVersionId: z - .string() - .describe( - 'ContentVersion record Id (15 or 18 chars). Get from SOQL on ContentVersion or ContentDocumentLink. Returns base64-encoded file content and content-type.' - ), - }), + input: lazySchema(() => + z.object({ + contentVersionId: z + .string() + .describe( + 'ContentVersion record Id (15 or 18 chars). Get from SOQL on ContentVersion or ContentDocumentLink. Returns base64-encoded file content and content-type.' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { contentVersionId: string }; const baseUrl = getBaseUrl(ctx.secrets?.tokenUrl as string | undefined); diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/servicenow_search/servicenow_search.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/servicenow_search/servicenow_search.ts index befd49ca9e338..d80901ccf8a53 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/servicenow_search/servicenow_search.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/servicenow_search/servicenow_search.ts @@ -21,7 +21,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; import { SearchInputSchema, @@ -87,17 +87,19 @@ export const ServicenowSearch: ConnectorSpec = { ], }, - schema: z.object({ - instanceUrl: z - .string() - .url() - .describe('ServiceNow instance URL (e.g., https://your-instance.service-now.com)') - .meta({ - label: 'Instance URL', - widget: 'text', - placeholder: 'https://your-instance.service-now.com', - }), - }), + schema: lazySchema(() => + z.object({ + instanceUrl: z + .string() + .url() + .describe('ServiceNow instance URL (e.g., https://your-instance.service-now.com)') + .meta({ + label: 'Instance URL', + widget: 'text', + placeholder: 'https://your-instance.service-now.com', + }), + }) + ), actions: { search: { @@ -255,11 +257,13 @@ export const ServicenowSearch: ConnectorSpec = { 'Attachment sys_ids can be found by querying the sys_attachment table: ' + 'use listRecords with table=sys_attachment and encodedQuery=table_name=^table_sys_id=.', input: GetAttachmentInputSchema, - output: z.object({ - fileName: z.string().describe('Name of the attachment file'), - contentType: z.string().describe('MIME type of the attachment'), - base64: z.string().describe('Base64-encoded attachment content'), - }), + output: lazySchema(() => + z.object({ + fileName: z.string().describe('Name of the attachment file'), + contentType: z.string().describe('MIME type of the attachment'), + base64: z.string().describe('Base64-encoded attachment content'), + }) + ), handler: async (ctx, input: GetAttachmentInput) => { const { instanceUrl } = ctx.config as { instanceUrl: string }; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/servicenow_search/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/servicenow_search/types.ts index 9cd5b52a9477d..5017fb5b210f5 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/servicenow_search/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/servicenow_search/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; /** * Common ServiceNow tables and their purpose, for use in field descriptions. @@ -30,103 +30,119 @@ const TABLE_DESCRIPTION = // Action input schemas & inferred types // ============================================================================= -export const SearchInputSchema = z.object({ - table: z.string().describe(TABLE_DESCRIPTION), - query: z.string().describe('Full-text search query string'), - encodedQuery: z - .string() - .optional() - .describe( - 'Optional ServiceNow encoded query to combine with the full-text search for additional filtering. ' + - 'Syntax: AND conditions with ^ (field1=value1^field2=value2), OR with ^OR (field1=value1^ORfield2=value2). ' + - 'Operators: = != < > LIKE STARTSWITH ENDSWITH ISEMPTY ISNOTEMPTY. ' + - 'Date ranges: sys_created_on>2024-01-01^sys_created_on<2025-01-01. ' + - 'Examples: active=true^priority=1 | state=1^ORstate=2 | assigned_toISEMPTY^active=true | short_descriptionLIKEnetwork^priority<=2' - ), - fields: z - .string() - .optional() - .describe( - 'Comma-separated list of fields to return (e.g., sys_id,number,short_description,description)' - ), - limit: z.number().default(20).describe('Maximum number of results to return (default: 20)'), - offset: z.number().optional().describe('Offset for pagination'), -}); +export const SearchInputSchema = lazySchema(() => + z.object({ + table: z.string().describe(TABLE_DESCRIPTION), + query: z.string().describe('Full-text search query string'), + encodedQuery: z + .string() + .optional() + .describe( + 'Optional ServiceNow encoded query to combine with the full-text search for additional filtering. ' + + 'Syntax: AND conditions with ^ (field1=value1^field2=value2), OR with ^OR (field1=value1^ORfield2=value2). ' + + 'Operators: = != < > LIKE STARTSWITH ENDSWITH ISEMPTY ISNOTEMPTY. ' + + 'Date ranges: sys_created_on>2024-01-01^sys_created_on<2025-01-01. ' + + 'Examples: active=true^priority=1 | state=1^ORstate=2 | assigned_toISEMPTY^active=true | short_descriptionLIKEnetwork^priority<=2' + ), + fields: z + .string() + .optional() + .describe( + 'Comma-separated list of fields to return (e.g., sys_id,number,short_description,description)' + ), + limit: z.number().default(20).describe('Maximum number of results to return (default: 20)'), + offset: z.number().optional().describe('Offset for pagination'), + }) +); export type SearchInput = z.infer; -export const GetRecordInputSchema = z.object({ - table: z.string().describe(TABLE_DESCRIPTION), - sysId: z.string().describe('The sys_id of the record to retrieve'), - fields: z.string().optional().describe('Comma-separated list of fields to return'), -}); +export const GetRecordInputSchema = lazySchema(() => + z.object({ + table: z.string().describe(TABLE_DESCRIPTION), + sysId: z.string().describe('The sys_id of the record to retrieve'), + fields: z.string().optional().describe('Comma-separated list of fields to return'), + }) +); export type GetRecordInput = z.infer; -export const ListRecordsInputSchema = z.object({ - table: z.string().describe(TABLE_DESCRIPTION), - encodedQuery: z - .string() - .optional() - .describe( - 'ServiceNow encoded query string for filtering. ' + - 'Syntax: AND conditions with ^ (field1=value1^field2=value2), OR with ^OR (field1=value1^ORfield2=value2). ' + - 'Operators: = != < > LIKE STARTSWITH ENDSWITH ISEMPTY ISNOTEMPTY. ' + - 'Date ranges: sys_created_on>2024-01-01^sys_created_on<2025-01-01. ' + - 'Examples: number=INC0010023 | active=true^priority=1 | state=1^ORstate=2 | assigned_toISEMPTY^active=true | assignment_group.nameLIKEnetwork^state!=6 | short_descriptionLIKEnetwork^priority<=2' - ), - fields: z.string().optional().describe('Comma-separated list of fields to return'), - limit: z.number().default(20).describe('Maximum number of results to return (default: 20)'), - offset: z.number().optional().describe('Offset for pagination'), - orderBy: z - .string() - .optional() - .describe('Field to order results by (prefix with - for descending)'), -}); +export const ListRecordsInputSchema = lazySchema(() => + z.object({ + table: z.string().describe(TABLE_DESCRIPTION), + encodedQuery: z + .string() + .optional() + .describe( + 'ServiceNow encoded query string for filtering. ' + + 'Syntax: AND conditions with ^ (field1=value1^field2=value2), OR with ^OR (field1=value1^ORfield2=value2). ' + + 'Operators: = != < > LIKE STARTSWITH ENDSWITH ISEMPTY ISNOTEMPTY. ' + + 'Date ranges: sys_created_on>2024-01-01^sys_created_on<2025-01-01. ' + + 'Examples: number=INC0010023 | active=true^priority=1 | state=1^ORstate=2 | assigned_toISEMPTY^active=true | assignment_group.nameLIKEnetwork^state!=6 | short_descriptionLIKEnetwork^priority<=2' + ), + fields: z.string().optional().describe('Comma-separated list of fields to return'), + limit: z.number().default(20).describe('Maximum number of results to return (default: 20)'), + offset: z.number().optional().describe('Offset for pagination'), + orderBy: z + .string() + .optional() + .describe('Field to order results by (prefix with - for descending)'), + }) +); export type ListRecordsInput = z.infer; -export const ListTablesInputSchema = z.object({ - query: z - .string() - .optional() - .describe('Optional filter to search table names or labels (e.g., "incident", "CMDB")'), - limit: z.number().default(50).describe('Maximum number of tables to return (default: 50)'), - offset: z.number().optional().describe('Offset for pagination'), -}); +export const ListTablesInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .optional() + .describe('Optional filter to search table names or labels (e.g., "incident", "CMDB")'), + limit: z.number().default(50).describe('Maximum number of tables to return (default: 50)'), + offset: z.number().optional().describe('Offset for pagination'), + }) +); export type ListTablesInput = z.infer; -export const ListKnowledgeBasesInputSchema = z.object({ - limit: z - .number() - .optional() - .default(20) - .describe('Maximum number of knowledge bases to return (default: 20)'), - offset: z.number().optional().describe('Offset for pagination'), -}); +export const ListKnowledgeBasesInputSchema = lazySchema(() => + z.object({ + limit: z + .number() + .optional() + .default(20) + .describe('Maximum number of knowledge bases to return (default: 20)'), + offset: z.number().optional().describe('Offset for pagination'), + }) +); export type ListKnowledgeBasesInput = z.infer; -export const GetCommentsInputSchema = z.object({ - tableName: z - .string() - .describe( - 'The ServiceNow table the record belongs to (e.g., incident, change_request, problem)' - ), - recordSysId: z - .string() - .describe('The sys_id of the record whose comments/work notes to retrieve'), - limit: z.number().default(20).describe('Maximum number of entries to return (default: 20)'), - offset: z.number().optional().describe('Offset for pagination'), -}); +export const GetCommentsInputSchema = lazySchema(() => + z.object({ + tableName: z + .string() + .describe( + 'The ServiceNow table the record belongs to (e.g., incident, change_request, problem)' + ), + recordSysId: z + .string() + .describe('The sys_id of the record whose comments/work notes to retrieve'), + limit: z.number().default(20).describe('Maximum number of entries to return (default: 20)'), + offset: z.number().optional().describe('Offset for pagination'), + }) +); export type GetCommentsInput = z.infer; -export const GetAttachmentInputSchema = z.object({ - sysId: z.string().describe('The sys_id of the attachment (from the sys_attachment table)'), -}); +export const GetAttachmentInputSchema = lazySchema(() => + z.object({ + sysId: z.string().describe('The sys_id of the attachment (from the sys_attachment table)'), + }) +); export type GetAttachmentInput = z.infer; -export const DescribeTableInputSchema = z.object({ - table: z - .string() - .describe( - 'The name of the ServiceNow table to describe (e.g., incident, kb_knowledge, change_request)' - ), -}); +export const DescribeTableInputSchema = lazySchema(() => + z.object({ + table: z + .string() + .describe( + 'The name of the ServiceNow table to describe (e.g., incident, kb_knowledge, change_request)' + ), + }) +); export type DescribeTableInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_online/sharepoint_online.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_online/sharepoint_online.ts index 6c4a9cc684dea..dd4ddc565b1eb 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_online/sharepoint_online.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_online/sharepoint_online.ts @@ -20,16 +20,18 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; /** * Common output schema for Microsoft Graph API responses that return a collection. * Uses z.any() for the array items to avoid over-specifying the response structure. */ -const GraphCollectionOutputSchema = z.object({ - value: z.array(z.any()).describe('Array of items returned from the API'), - '@odata.nextLink': z.string().optional().describe('URL to fetch next page of results'), -}); +const GraphCollectionOutputSchema = lazySchema(() => + z.object({ + value: z.array(z.any()).describe('Array of items returned from the API'), + '@odata.nextLink': z.string().optional().describe('URL to fetch next page of results'), + }) +); export const SharepointOnline: ConnectorSpec = { metadata: { @@ -165,13 +167,15 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'List all pages in a SharePoint site. Returns page metadata (id, title, description, webUrl, createdDateTime, lastModifiedDateTime). Use getAllSites to discover siteId values, and then use getSitePageContents to fetch the full content of a specific page.', - input: z.object({ - siteId: z - .string() - .describe( - 'The ID of the SharePoint site whose pages you want to list. Use getAllSites to discover site IDs.' - ), - }), + input: lazySchema(() => + z.object({ + siteId: z + .string() + .describe( + 'The ID of the SharePoint site whose pages you want to list. Use getAllSites to discover site IDs.' + ), + }) + ), output: GraphCollectionOutputSchema, handler: async (ctx, input) => { const typedInput = input as { @@ -199,18 +203,20 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'Fetch the full HTML content of a SharePoint site page, including its canvas layout. Use this to read wiki/news pages. Use getAllSites to discover siteId values, and getSitePages to discover pageId values for a given site.', - input: z.object({ - siteId: z - .string() - .describe( - 'The ID of the SharePoint site that contains the page. Use getAllSites to discover site IDs.' - ), - pageId: z - .string() - .describe( - 'The ID of the page to fetch. Use getSitePages to list pages and discover their IDs for a given site.' - ), - }), + input: lazySchema(() => + z.object({ + siteId: z + .string() + .describe( + 'The ID of the SharePoint site that contains the page. Use getAllSites to discover site IDs.' + ), + pageId: z + .string() + .describe( + 'The ID of the page to fetch. Use getSitePages to list pages and discover their IDs for a given site.' + ), + }) + ), output: z.any(), handler: async (ctx, input) => { const typedInput = input as { @@ -245,26 +251,28 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'Retrieve details for a single SharePoint site by either its site ID or its relative URL. Returns id, displayName, webUrl, siteCollection, createdDateTime, and lastModifiedDateTime. Use getAllSites to discover site IDs, or provide a relativeUrl in the format "contoso.sharepoint.com:/sites/hr:".', - input: z.union([ - z - .object({ - siteId: z - .string() - .describe( - 'The ID of the SharePoint site to retrieve. Use getAllSites to discover site IDs.' - ), - }) - .strict(), - z - .object({ - relativeUrl: z - .string() - .describe( - 'The relative URL of the site as a path in the format "hostname:/path:", e.g. "contoso.sharepoint.com:/sites/hr:". Use this as an alternative to siteId when you know the URL but not the ID.' - ), - }) - .strict(), - ]), + input: lazySchema(() => + z.union([ + z + .object({ + siteId: z + .string() + .describe( + 'The ID of the SharePoint site to retrieve. Use getAllSites to discover site IDs.' + ), + }) + .strict(), + z + .object({ + relativeUrl: z + .string() + .describe( + 'The relative URL of the site as a path in the format "hostname:/path:", e.g. "contoso.sharepoint.com:/sites/hr:". Use this as an alternative to siteId when you know the URL but not the ID.' + ), + }) + .strict(), + ]) + ), handler: async (ctx, input) => { const typedInput = input as { siteId: string } | { relativeUrl: string }; const hasSiteId = 'siteId' in typedInput && typedInput.siteId; @@ -296,13 +304,15 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'List all document libraries (drives) within a SharePoint site. Returns drive metadata including id, name, driveType, webUrl, and owner. Use getAllSites to discover siteId values. Drive IDs returned here are required by getDriveItems and downloadDriveItem.', - input: z.object({ - siteId: z - .string() - .describe( - 'The ID of the SharePoint site whose document libraries (drives) you want to list. Use getAllSites to discover site IDs.' - ), - }), + input: lazySchema(() => + z.object({ + siteId: z + .string() + .describe( + 'The ID of the SharePoint site whose document libraries (drives) you want to list. Use getAllSites to discover site IDs.' + ), + }) + ), output: GraphCollectionOutputSchema, handler: async (ctx, input) => { const typedInput = input as { @@ -332,15 +342,17 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'List all SharePoint lists within a site (e.g., custom lists, document libraries represented as lists). Returns id, displayName, name, webUrl, and description for each list. Use getAllSites to discover siteId values. List IDs returned here are required by getSiteListItems.', - input: z - .object({ - siteId: z - .string() - .describe( - 'The ID of the SharePoint site whose lists you want to enumerate. Use getAllSites to discover site IDs.' - ), - }) - .strict(), + input: lazySchema(() => + z + .object({ + siteId: z + .string() + .describe( + 'The ID of the SharePoint site whose lists you want to enumerate. Use getAllSites to discover site IDs.' + ), + }) + .strict() + ), output: GraphCollectionOutputSchema, handler: async (ctx, input) => { const typedInput = input as { @@ -370,18 +382,20 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'Fetch all items from a specific list within a SharePoint site. Returns item metadata (id, webUrl, createdDateTime, lastModifiedDateTime, createdBy, lastModifiedBy). Use getAllSites to discover siteId values and getSiteLists to discover listId values.', - input: z.object({ - siteId: z - .string() - .describe( - 'The ID of the SharePoint site that owns the list. Use getAllSites to discover site IDs.' - ), - listId: z - .string() - .describe( - 'The ID of the list whose items you want to retrieve. Use getSiteLists to discover list IDs for a given site.' - ), - }), + input: lazySchema(() => + z.object({ + siteId: z + .string() + .describe( + 'The ID of the SharePoint site that owns the list. Use getAllSites to discover site IDs.' + ), + listId: z + .string() + .describe( + 'The ID of the list whose items you want to retrieve. Use getSiteLists to discover list IDs for a given site.' + ), + }) + ), output: GraphCollectionOutputSchema, handler: async (ctx, input) => { const typedInput = input as { @@ -418,19 +432,21 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'List files and folders within a SharePoint document library (drive), optionally scoped to a subfolder path. Returns item metadata including id, name, webUrl, size, and @microsoft.graph.downloadUrl. Use getSiteDrives to discover driveId values. The @microsoft.graph.downloadUrl field can be passed to downloadItemFromURL.', - input: z.object({ - driveId: z - .string() - .describe( - 'The ID of the document library (drive) to browse. Use getSiteDrives to discover drive IDs for a site.' - ), - path: z - .string() - .optional() - .describe( - 'Optional relative path within the drive root to scope the listing (e.g. "Folder/Subfolder"). Omit to list the root of the drive.' - ), - }), + input: lazySchema(() => + z.object({ + driveId: z + .string() + .describe( + 'The ID of the document library (drive) to browse. Use getSiteDrives to discover drive IDs for a site.' + ), + path: z + .string() + .optional() + .describe( + 'Optional relative path within the drive root to scope the listing (e.g. "Folder/Subfolder"). Omit to list the root of the drive.' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { driveId: string; path?: string }; if (!typedInput.driveId) { @@ -458,23 +474,27 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'Download the content of a file from a SharePoint document library and return it as UTF-8 text. Best suited for plain-text or markdown files. For PDFs, .docx, and other binary formats that require preprocessing, use downloadItemFromURL instead (which returns base64 for Elasticsearch ingest pipeline extraction). Use getSiteDrives to find driveId and getDriveItems to find itemId.', - input: z.object({ - driveId: z - .string() - .describe( - 'The ID of the document library (drive) that contains the file. Use getSiteDrives to discover drive IDs.' - ), - itemId: z - .string() - .describe( - 'The ID of the file item to download. Use getDriveItems to list items in a drive and discover their IDs.' - ), - }), - output: z.object({ - contentType: z.string().optional().describe('Content-Type header'), - contentLength: z.string().optional().describe('Content-Length header'), - text: z.string().describe('File content as UTF-8 text'), - }), + input: lazySchema(() => + z.object({ + driveId: z + .string() + .describe( + 'The ID of the document library (drive) that contains the file. Use getSiteDrives to discover drive IDs.' + ), + itemId: z + .string() + .describe( + 'The ID of the file item to download. Use getDriveItems to list items in a drive and discover their IDs.' + ), + }) + ), + output: lazySchema(() => + z.object({ + contentType: z.string().optional().describe('Content-Type header'), + contentLength: z.string().optional().describe('Content-Length header'), + text: z.string().describe('File content as UTF-8 text'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { driveId: string; @@ -508,19 +528,23 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'Download a SharePoint file using its pre-authenticated @microsoft.graph.downloadUrl and return the content as a base64-encoded string. Use this for PDFs, .docx, and other binary formats that require preprocessing via an Elasticsearch ingest pipeline attachment processor. For plain-text or markdown files you can use downloadDriveItem instead. Use getDriveItems to find the @microsoft.graph.downloadUrl field on a file item.', - input: z.object({ - downloadUrl: z - .string() - .url() - .describe( - 'The pre-authenticated download URL for the file. This is the @microsoft.graph.downloadUrl property returned by getDriveItems. Note: these URLs are time-limited and should be used promptly.' - ), - }), - output: z.object({ - contentType: z.string().optional().describe('Content-Type header'), - contentLength: z.string().optional().describe('Content-Length header'), - base64: z.string().describe('File content as base64-encoded string'), - }), + input: lazySchema(() => + z.object({ + downloadUrl: z + .string() + .url() + .describe( + 'The pre-authenticated download URL for the file. This is the @microsoft.graph.downloadUrl property returned by getDriveItems. Note: these URLs are time-limited and should be used promptly.' + ), + }) + ), + output: lazySchema(() => + z.object({ + contentType: z.string().optional().describe('Content-Type header'), + contentLength: z.string().optional().describe('Content-Length header'), + base64: z.string().describe('File content as base64-encoded string'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { downloadUrl: string; @@ -547,23 +571,25 @@ export const SharepointOnline: ConnectorSpec = { callGraphAPI: { isTool: true, description: 'Call a Microsoft Graph v1.0 endpoint by path only (e.g., /v1.0/me).', - input: z.object({ - method: z.enum(['GET', 'POST']).describe('HTTP method'), - path: z - .string() - .describe("Graph path starting with '/v1.0/' (e.g., '/v1.0/me')") - .refine((value) => value.startsWith('/v1.0/'), { - message: "Path must start with '/v1.0/'", - }) - .refine((value) => !/^https?:\/\//i.test(value), { - message: 'Path must not be a full URL', - }), - query: z - .record(z.string(), z.union([z.string(), z.number(), z.boolean()])) - .optional() - .describe('Query parameters (e.g., $top, $filter)'), - body: z.any().optional().describe('Request body (for POST)'), - }), + input: lazySchema(() => + z.object({ + method: z.enum(['GET', 'POST']).describe('HTTP method'), + path: z + .string() + .describe("Graph path starting with '/v1.0/' (e.g., '/v1.0/me')") + .refine((value) => value.startsWith('/v1.0/'), { + message: "Path must start with '/v1.0/'", + }) + .refine((value) => !/^https?:\/\//i.test(value), { + message: 'Path must not be a full URL', + }), + query: z + .record(z.string(), z.union([z.string(), z.number(), z.boolean()])) + .optional() + .describe('Query parameters (e.g., $top, $filter)'), + body: z.any().optional().describe('Request body (for POST)'), + }) + ), output: z.any(), handler: async (ctx, input) => { const typedInput = input as { @@ -596,37 +622,39 @@ export const SharepointOnline: ConnectorSpec = { isTool: true, description: 'Search SharePoint content using the Microsoft Graph Search API with Keyword Query Language (KQL). Supports searching across sites, lists, list items, drives, and drive items. Note: not all entity type combinations can be mixed in a single request — valid groupings are (driveItem, listItem), (site, list), or (drive) alone.', - input: z.object({ - query: z - .string() - .describe( - 'KQL search query string. Examples: "contoso product", "filename:budget filetype:xlsx", "author:jane AND filetype:docx". Supports standard KQL operators (AND, OR, NOT) and property restrictions.' - ), - entityTypes: z - .array(z.enum(['site', 'list', 'listItem', 'drive', 'driveItem'])) - .optional() - .describe( - 'Entity types to include in the search. Valid groupings (cannot be mixed arbitrarily): (driveItem, listItem), (site, list), or (drive) alone. Defaults to ["site"] if omitted.' - ), - region: z - .enum(['NAM', 'EUR', 'APC', 'LAM', 'MEA']) - .optional() - .describe( - 'Search region. Only used with app-only (client credentials) auth — ignored for delegated auth. NAM=North America, EUR=Europe, APC=Asia Pacific, LAM=Latin America, MEA=Middle East/Africa. Defaults to NAM when using app-only auth.' - ), - from: z - .number() - .default(0) - .describe('Zero-based pagination offset (number of results to skip). Defaults to 0.'), - size: z - .number() - .min(1) - .max(500) - .default(25) - .describe( - 'Number of results to return per page. Must be between 1 and 500. Defaults to 25.' - ), - }), + input: lazySchema(() => + z.object({ + query: z + .string() + .describe( + 'KQL search query string. Examples: "contoso product", "filename:budget filetype:xlsx", "author:jane AND filetype:docx". Supports standard KQL operators (AND, OR, NOT) and property restrictions.' + ), + entityTypes: z + .array(z.enum(['site', 'list', 'listItem', 'drive', 'driveItem'])) + .optional() + .describe( + 'Entity types to include in the search. Valid groupings (cannot be mixed arbitrarily): (driveItem, listItem), (site, list), or (drive) alone. Defaults to ["site"] if omitted.' + ), + region: z + .enum(['NAM', 'EUR', 'APC', 'LAM', 'MEA']) + .optional() + .describe( + 'Search region. Only used with app-only (client credentials) auth — ignored for delegated auth. NAM=North America, EUR=Europe, APC=Asia Pacific, LAM=Latin America, MEA=Middle East/Africa. Defaults to NAM when using app-only auth.' + ), + from: z + .number() + .default(0) + .describe('Zero-based pagination offset (number of results to skip). Defaults to 0.'), + size: z + .number() + .min(1) + .max(500) + .default(25) + .describe( + 'Number of results to return per page. Must be between 1 and 500. Defaults to 25.' + ), + }) + ), output: z.any(), handler: async (ctx, input) => { const typedInput = input as { diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_server/sharepoint_server.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_server/sharepoint_server.ts index 3d0d85a0aacdd..5449b7722d35d 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_server/sharepoint_server.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_server/sharepoint_server.ts @@ -22,7 +22,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; import { CallRestApiInputSchema, @@ -55,18 +55,20 @@ export const SharepointServer: ConnectorSpec = { types: ['basic'], }, - schema: z.object({ - siteUrl: z - .string() - .url() - .transform((val) => val.replace(/\/+$/, '')) - .describe('SharePoint Server site URL') - .meta({ - label: 'Site URL', - widget: 'text', - placeholder: 'https://sharepoint.company.com/sites/mysite', - }), - }), + schema: lazySchema(() => + z.object({ + siteUrl: z + .string() + .url() + .transform((val) => val.replace(/\/+$/, '')) + .describe('SharePoint Server site URL') + .meta({ + label: 'Site URL', + widget: 'text', + placeholder: 'https://sharepoint.company.com/sites/mysite', + }), + }) + ), actions: { getWeb: { diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_server/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_server/types.ts index ed3fdb235ae28..5ba48aac02132 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_server/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/sharepoint_server/types.ts @@ -7,73 +7,91 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; -export const ODataCollectionOutputSchema = z.object({ - value: z.array(z.any()).describe('Array of items returned from the API'), -}); +export const ODataCollectionOutputSchema = lazySchema(() => + z.object({ + value: z.array(z.any()).describe('Array of items returned from the API'), + }) +); -export const GetListItemsInputSchema = z.object({ - listTitle: z - .string() - .describe( - "Exact display name of the list, as returned in the Title field of getLists. Case-sensitive. Example: 'Documents', 'Tasks', 'Site Pages'" - ), -}); +export const GetListItemsInputSchema = lazySchema(() => + z.object({ + listTitle: z + .string() + .describe( + "Exact display name of the list, as returned in the Title field of getLists. Case-sensitive. Example: 'Documents', 'Tasks', 'Site Pages'" + ), + }) +); -export const GetFolderContentsInputSchema = z.object({ - path: z - .string() - .describe( - "Server-relative URL of the folder: starts with '/', no hostname. Get this from getLists (RootFolder.ServerRelativeUrl) or from a previous getFolderContents result (ServerRelativeUrl on a folder). Example: '/sites/mysite/Shared Documents' or '/sites/mysite/Shared Documents/Reports'" - ), -}); +export const GetFolderContentsInputSchema = lazySchema(() => + z.object({ + path: z + .string() + .describe( + "Server-relative URL of the folder: starts with '/', no hostname. Get this from getLists (RootFolder.ServerRelativeUrl) or from a previous getFolderContents result (ServerRelativeUrl on a folder). Example: '/sites/mysite/Shared Documents' or '/sites/mysite/Shared Documents/Reports'" + ), + }) +); -export const GetFolderContentsOutputSchema = z.object({ - files: z.array(z.any()).describe('Files in the folder'), - folders: z.array(z.any()).describe('Subfolders in the folder'), -}); +export const GetFolderContentsOutputSchema = lazySchema(() => + z.object({ + files: z.array(z.any()).describe('Files in the folder'), + folders: z.array(z.any()).describe('Subfolders in the folder'), + }) +); -export const DownloadFileInputSchema = z.object({ - path: z - .string() - .describe( - "Server-relative URL of the file: starts with '/', no hostname. Get this from the ServerRelativeUrl field in getFolderContents results. Example: '/sites/mysite/Shared Documents/report.txt'" - ), -}); +export const DownloadFileInputSchema = lazySchema(() => + z.object({ + path: z + .string() + .describe( + "Server-relative URL of the file: starts with '/', no hostname. Get this from the ServerRelativeUrl field in getFolderContents results. Example: '/sites/mysite/Shared Documents/report.txt'" + ), + }) +); -export const DownloadFileOutputSchema = z.object({ - contentType: z.string().optional().describe('Content-Type header'), - contentLength: z.string().optional().describe('Content-Length header'), - text: z.string().describe('File content as UTF-8 text'), -}); +export const DownloadFileOutputSchema = lazySchema(() => + z.object({ + contentType: z.string().optional().describe('Content-Type header'), + contentLength: z.string().optional().describe('Content-Length header'), + text: z.string().describe('File content as UTF-8 text'), + }) +); -export const GetSitePageContentsInputSchema = z.object({ - pageId: z - .number() - .int() - .describe( - "Integer item ID of the page. Get this from getListItems with listTitle='Site Pages': look for the Id field (not the GUID) on the desired page. Example: 3" - ), -}); +export const GetSitePageContentsInputSchema = lazySchema(() => + z.object({ + pageId: z + .number() + .int() + .describe( + "Integer item ID of the page. Get this from getListItems with listTitle='Site Pages': look for the Id field (not the GUID) on the desired page. Example: 3" + ), + }) +); -export const SearchInputSchema = z.object({ - query: z - .string() - .describe( - "KQL query string. Use plain keywords for broad search, or field:value pairs for filtered search. Examples: 'budget report', 'FileExtension:docx', 'author:Jane AND project plan', 'ContentType:Document AND title:policy'" - ), - from: z.number().default(0).describe('Zero-based start row for pagination (default: 0)'), - size: z.number().default(10).describe('Number of results to return (default: 10)'), -}); +export const SearchInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .describe( + "KQL query string. Use plain keywords for broad search, or field:value pairs for filtered search. Examples: 'budget report', 'FileExtension:docx', 'author:Jane AND project plan', 'ContentType:Document AND title:policy'" + ), + from: z.number().default(0).describe('Zero-based start row for pagination (default: 0)'), + size: z.number().default(10).describe('Number of results to return (default: 10)'), + }) +); -export const CallRestApiInputSchema = z.object({ - method: z.enum(['GET', 'POST']).describe('HTTP method'), - path: z - .string() - .describe("API path starting with '_api/' (for example, '_api/web/title')") - .refine((value) => value.startsWith('_api/'), { - message: "Path must start with '_api/'", - }), - body: z.any().optional().describe('Request body (for POST)'), -}); +export const CallRestApiInputSchema = lazySchema(() => + z.object({ + method: z.enum(['GET', 'POST']).describe('HTTP method'), + path: z + .string() + .describe("API path starting with '_api/' (for example, '_api/web/title')") + .refine((value) => value.startsWith('_api/'), { + message: "Path must start with '_api/'", + }), + body: z.any().optional().describe('Request body (for POST)'), + }) +); diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/shodan/shodan.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/shodan/shodan.ts index 96291c2432f7d..10d4dbafef10f 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/shodan/shodan.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/shodan/shodan.ts @@ -19,7 +19,7 @@ * MVP implementation focusing on core asset discovery actions. */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { i18n } from '@kbn/i18n'; import type { ConnectorSpec } from '../../connector_spec'; @@ -41,10 +41,12 @@ export const ShodanConnector: ConnectorSpec = { actions: { searchHosts: { isTool: true, - input: z.object({ - query: z.string().describe('Search query'), - page: z.number().int().min(1).optional().default(1).describe('Page number'), - }), + input: lazySchema(() => + z.object({ + query: z.string().describe('Search query'), + page: z.number().int().min(1).optional().default(1).describe('Page number'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { query: string; page?: number }; const apiKey = ctx.secrets?.authType === 'api_key_header' ? ctx.secrets['X-Api-Key'] : ''; @@ -65,9 +67,11 @@ export const ShodanConnector: ConnectorSpec = { getHostInfo: { isTool: true, - input: z.object({ - ip: z.ipv4().describe('IP address'), - }), + input: lazySchema(() => + z.object({ + ip: z.ipv4().describe('IP address'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { ip: string }; const apiKey = ctx.secrets?.authType === 'api_key_header' ? ctx.secrets['X-Api-Key'] : ''; @@ -91,10 +95,12 @@ export const ShodanConnector: ConnectorSpec = { countResults: { isTool: true, - input: z.object({ - query: z.string().describe('Search query'), - facets: z.string().optional().describe('Facets to include'), - }), + input: lazySchema(() => + z.object({ + query: z.string().describe('Search query'), + facets: z.string().optional().describe('Facets to include'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { query: string; facets?: string }; const apiKey = ctx.secrets?.authType === 'api_key_header' ? ctx.secrets['X-Api-Key'] : ''; @@ -114,7 +120,7 @@ export const ShodanConnector: ConnectorSpec = { getServices: { isTool: true, - input: z.object({}), + input: lazySchema(() => z.object({})), handler: async (ctx) => { const apiKey = ctx.secrets?.authType === 'api_key_header' ? ctx.secrets['X-Api-Key'] : ''; const response = await ctx.client.get('https://api.shodan.io/shodan/services', { diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/slack/slack.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/slack/slack.ts index e2eae144ccc15..38ba8fa8ea8cb 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/slack/slack.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/slack/slack.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { AxiosError, AxiosResponse } from 'axios'; import type { ConnectorSpec, ActionContext } from '../../connector_spec'; import { @@ -206,7 +206,7 @@ export const Slack: ConnectorSpec = { }, // No additional configuration needed beyond OAuth credentials - schema: z.object({}), + schema: lazySchema(() => z.object({})), actions: { // https://api.slack.com/methods/assistant.search.context diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/slack/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/slack/types.ts index 091e62a39120a..007d40d79a311 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/slack/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/slack/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Slack Web API response types (minimal shapes used by this connector spec) @@ -78,196 +78,213 @@ const slackConversationTypesWithPublicDefault = () => val && val.length > 0 ? val : ['public_channel'] ); -export const SlackResolveChannelIdInputSchema = z.object({ - name: z - .string() - .min(1) - .describe( - 'Channel name to resolve (e.g. "general" or "#general"). Returns the first matching conversation ID (C.../G...). To list or browse channels (e.g. what is available), use listChannels instead of probing many names here.' +export const SlackResolveChannelIdInputSchema = lazySchema(() => + z.object({ + name: z + .string() + .min(1) + .describe( + 'Channel name to resolve (e.g. "general" or "#general"). Returns the first matching conversation ID (C.../G...). To list or browse channels (e.g. what is available), use listChannels instead of probing many names here.' + ), + types: slackConversationTypesWithPublicDefault().describe( + 'Conversation types to search. Defaults to public_channel. Valid: public_channel, private_channel, im, mpim.' ), - types: slackConversationTypesWithPublicDefault().describe( - 'Conversation types to search. Defaults to public_channel. Valid: public_channel, private_channel, im, mpim.' - ), - match: z - .enum(['exact', 'contains']) - .default('exact') - .describe( - 'How to match the channel name. exact is fastest/most precise. contains is for a partial name you already know (e.g. a word from the channel name); do not use contains with very short strings to scan or discover channels — use listChannels for discovery.' - ), - excludeArchived: z.boolean().default(true).describe('Exclude archived channels (default true)'), - cursor: z - .string() - .optional() - .describe('Optional cursor to resume a previous scan (advanced). Usually omit.'), - limit: z - .number() - .int() - .min(1) - .max(SLACK_MAX_CONVERSATIONS_LIST_LIMIT) - .default(SLACK_DEFAULT_CONVERSATIONS_LIST_LIMIT) - .describe( - `Channels per page to request (1-${SLACK_MAX_CONVERSATIONS_LIST_LIMIT}). Defaults to ${SLACK_DEFAULT_CONVERSATIONS_LIST_LIMIT}.` - ), - maxPages: z - .number() - .int() - .min(1) - .max(SLACK_MAX_RESOLVE_CHANNEL_MAX_PAGES) - .default(SLACK_DEFAULT_RESOLVE_CHANNEL_MAX_PAGES) - .describe( - `Maximum number of pages to scan before giving up. Defaults to ${SLACK_DEFAULT_RESOLVE_CHANNEL_MAX_PAGES}.` - ), -}); + match: z + .enum(['exact', 'contains']) + .default('exact') + .describe( + 'How to match the channel name. exact is fastest/most precise. contains is for a partial name you already know (e.g. a word from the channel name); do not use contains with very short strings to scan or discover channels — use listChannels for discovery.' + ), + excludeArchived: z.boolean().default(true).describe('Exclude archived channels (default true)'), + cursor: z + .string() + .optional() + .describe('Optional cursor to resume a previous scan (advanced). Usually omit.'), + limit: z + .number() + .int() + .min(1) + .max(SLACK_MAX_CONVERSATIONS_LIST_LIMIT) + .default(SLACK_DEFAULT_CONVERSATIONS_LIST_LIMIT) + .describe( + `Channels per page to request (1-${SLACK_MAX_CONVERSATIONS_LIST_LIMIT}). Defaults to ${SLACK_DEFAULT_CONVERSATIONS_LIST_LIMIT}.` + ), + maxPages: z + .number() + .int() + .min(1) + .max(SLACK_MAX_RESOLVE_CHANNEL_MAX_PAGES) + .default(SLACK_DEFAULT_RESOLVE_CHANNEL_MAX_PAGES) + .describe( + `Maximum number of pages to scan before giving up. Defaults to ${SLACK_DEFAULT_RESOLVE_CHANNEL_MAX_PAGES}.` + ), + }) +); export type SlackResolveChannelIdInput = z.infer; -export const SlackListChannelsInputSchema = z.object({ - types: slackConversationTypesWithPublicDefault().describe( - 'Conversation types to list. Defaults to public_channel only. Valid: public_channel, private_channel, im, mpim.' - ), - excludeArchived: z.boolean().default(true).describe('Exclude archived channels (default true)'), - cursor: z - .string() - .optional() - .describe( - 'Pagination cursor from a previous listChannels response (nextCursor). Omit for the first page.' +export const SlackListChannelsInputSchema = lazySchema(() => + z.object({ + types: slackConversationTypesWithPublicDefault().describe( + 'Conversation types to list. Defaults to public_channel only. Valid: public_channel, private_channel, im, mpim.' ), - limit: z - .number() - .int() - .min(1) - .max(SLACK_MAX_CONVERSATIONS_LIST_LIMIT) - .default(SLACK_DEFAULT_CONVERSATIONS_LIST_LIMIT) - .describe( - `Channels per page to request (1-${SLACK_MAX_CONVERSATIONS_LIST_LIMIT}). Defaults to ${SLACK_DEFAULT_CONVERSATIONS_LIST_LIMIT}.` - ), - raw: z - .boolean() - .optional() - .describe( - 'Return the full raw Slack API response instead of a compact, LLM-friendly result. Defaults to false.' - ), -}); + excludeArchived: z.boolean().default(true).describe('Exclude archived channels (default true)'), + cursor: z + .string() + .optional() + .describe( + 'Pagination cursor from a previous listChannels response (nextCursor). Omit for the first page.' + ), + limit: z + .number() + .int() + .min(1) + .max(SLACK_MAX_CONVERSATIONS_LIST_LIMIT) + .default(SLACK_DEFAULT_CONVERSATIONS_LIST_LIMIT) + .describe( + `Channels per page to request (1-${SLACK_MAX_CONVERSATIONS_LIST_LIMIT}). Defaults to ${SLACK_DEFAULT_CONVERSATIONS_LIST_LIMIT}.` + ), + raw: z + .boolean() + .optional() + .describe( + 'Return the full raw Slack API response instead of a compact, LLM-friendly result. Defaults to false.' + ), + }) +); export type SlackListChannelsInput = z.infer; const SLACK_MAX_SEARCH_RESULTS_PER_PAGE = 20; export const SLACK_SEARCH_DEFAULT_COUNT = SLACK_MAX_SEARCH_RESULTS_PER_PAGE; -export const SlackSearchMessagesInputSchema = z.object({ - query: z - .string() - .min(1) - .describe( - 'Plain text search query to find messages. Do NOT embed Slack search operators like from: or in: here — use the dedicated fromUser, inChannel, after, and before parameters instead. Keep queries focused on a few keywords rather than long phrases for better results.' - ), - inChannel: z - .string() - .optional() - .describe( - 'Optional Slack search constraint. Adds `in:CHANNEL_NAME` to the query (e.g. in:general).' - ), - fromUser: z - .string() - .optional() - .describe( - "Optional Slack search constraint. Adds `from:USER_ID` (e.g. from:U012ABCDEF) or `from:username` to the query. Accepts a Slack username or user ID, NOT a full name. If you only know a person's full name, search for it as keywords in the query parameter first, then use the sender.username field from results for subsequent filtered searches." - ), - after: z - .string() - .optional() - .describe( - 'Optional Slack search constraint. Adds `after:YYYY-MM-DD` to the query (e.g. after:2026-02-10).' - ), - before: z - .string() - .optional() - .describe( - 'Optional Slack search constraint. Adds `before:YYYY-MM-DD` to the query (e.g. before:2026-02-10).' - ), - sort: z - .enum(['score', 'timestamp']) - .optional() - .describe('Sort order: score (relevance) or timestamp'), - sortDir: z.enum(['asc', 'desc']).optional().describe('Sort direction'), - count: z - .number() - .int() - .min(1) - .max(SLACK_MAX_SEARCH_RESULTS_PER_PAGE) - .optional() - .describe( - `Number of results to return (1-${SLACK_MAX_SEARCH_RESULTS_PER_PAGE}). Slack returns up to ${SLACK_MAX_SEARCH_RESULTS_PER_PAGE} results per page.` - ), - cursor: z - .string() - .optional() - .describe( - 'Pagination cursor to fetch the next page of results (use response_metadata.next_cursor from a previous call).' - ), - includeContextMessages: z - .boolean() - .optional() - .describe( - 'Include contextual messages (messages before/after the matched message, or thread context). Defaults to true.' - ), - includeBots: z.boolean().optional().describe('Include bot-authored messages. Defaults to false.'), - includeMessageBlocks: z - .boolean() - .optional() - .describe( - 'Include Block Kit blocks in message results (useful for extracting mentions/links). Defaults to true.' - ), - raw: z - .boolean() - .optional() - .describe('Return the full raw Slack API response instead of a compact, LLM-friendly result.'), -}); +export const SlackSearchMessagesInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .min(1) + .describe( + 'Plain text search query to find messages. Do NOT embed Slack search operators like from: or in: here — use the dedicated fromUser, inChannel, after, and before parameters instead. Keep queries focused on a few keywords rather than long phrases for better results.' + ), + inChannel: z + .string() + .optional() + .describe( + 'Optional Slack search constraint. Adds `in:CHANNEL_NAME` to the query (e.g. in:general).' + ), + fromUser: z + .string() + .optional() + .describe( + "Optional Slack search constraint. Adds `from:USER_ID` (e.g. from:U012ABCDEF) or `from:username` to the query. Accepts a Slack username or user ID, NOT a full name. If you only know a person's full name, search for it as keywords in the query parameter first, then use the sender.username field from results for subsequent filtered searches." + ), + after: z + .string() + .optional() + .describe( + 'Optional Slack search constraint. Adds `after:YYYY-MM-DD` to the query (e.g. after:2026-02-10).' + ), + before: z + .string() + .optional() + .describe( + 'Optional Slack search constraint. Adds `before:YYYY-MM-DD` to the query (e.g. before:2026-02-10).' + ), + sort: z + .enum(['score', 'timestamp']) + .optional() + .describe('Sort order: score (relevance) or timestamp'), + sortDir: z.enum(['asc', 'desc']).optional().describe('Sort direction'), + count: z + .number() + .int() + .min(1) + .max(SLACK_MAX_SEARCH_RESULTS_PER_PAGE) + .optional() + .describe( + `Number of results to return (1-${SLACK_MAX_SEARCH_RESULTS_PER_PAGE}). Slack returns up to ${SLACK_MAX_SEARCH_RESULTS_PER_PAGE} results per page.` + ), + cursor: z + .string() + .optional() + .describe( + 'Pagination cursor to fetch the next page of results (use response_metadata.next_cursor from a previous call).' + ), + includeContextMessages: z + .boolean() + .optional() + .describe( + 'Include contextual messages (messages before/after the matched message, or thread context). Defaults to true.' + ), + includeBots: z + .boolean() + .optional() + .describe('Include bot-authored messages. Defaults to false.'), + includeMessageBlocks: z + .boolean() + .optional() + .describe( + 'Include Block Kit blocks in message results (useful for extracting mentions/links). Defaults to true.' + ), + raw: z + .boolean() + .optional() + .describe( + 'Return the full raw Slack API response instead of a compact, LLM-friendly result.' + ), + }) +); export type SlackSearchMessagesInput = z.infer; -export const SlackCreateConversationInputSchema = z.object({ - name: z - .string() - .min(1) - .describe( - 'Name of the channel to create. Channel names can only contain lowercase letters, numbers, hyphens, and underscores, and must be 80 characters or fewer.' - ), - isPrivate: z - .boolean() - .optional() - .describe('Whether to create a private channel. Defaults to false (public).'), -}); +export const SlackCreateConversationInputSchema = lazySchema(() => + z.object({ + name: z + .string() + .min(1) + .describe( + 'Name of the channel to create. Channel names can only contain lowercase letters, numbers, hyphens, and underscores, and must be 80 characters or fewer.' + ), + isPrivate: z + .boolean() + .optional() + .describe('Whether to create a private channel. Defaults to false (public).'), + }) +); export type SlackCreateConversationInput = z.infer; -export const SlackInviteToConversationInputSchema = z.object({ - channel: z - .string() - .min(1) - .describe('The ID of the channel to invite users to (e.g. C... or G...).'), - users: z - .string() - .min(1) - .describe( - 'Comma-separated list of user IDs to invite to the channel (e.g. U01PWE77HD2,U02ABC1234).' - ), -}); +export const SlackInviteToConversationInputSchema = lazySchema(() => + z.object({ + channel: z + .string() + .min(1) + .describe('The ID of the channel to invite users to (e.g. C... or G...).'), + users: z + .string() + .min(1) + .describe( + 'Comma-separated list of user IDs to invite to the channel (e.g. U01PWE77HD2,U02ABC1234).' + ), + }) +); export type SlackInviteToConversationInput = z.infer; -export const SlackSendMessageInputSchema = z.object({ - channel: z - .string() - .min(1) - .describe( - 'Conversation ID to send the message to (e.g. C... for channels, G... for private channels, D... for DMs). Use listChannels to browse available channels, or resolveChannelId when you know the channel name and need its ID.' - ), - text: z.string().min(1).describe('The message text to send'), - threadTs: z - .string() - .optional() - .describe('Timestamp of another message to reply to (creates a threaded reply)'), - unfurlLinks: z - .boolean() - .optional() - .describe('Whether to enable unfurling of primarily text-based content'), - unfurlMedia: z.boolean().optional().describe('Whether to enable unfurling of media content'), -}); +export const SlackSendMessageInputSchema = lazySchema(() => + z.object({ + channel: z + .string() + .min(1) + .describe( + 'Conversation ID to send the message to (e.g. C... for channels, G... for private channels, D... for DMs). Use listChannels to browse available channels, or resolveChannelId when you know the channel name and need its ID.' + ), + text: z.string().min(1).describe('The message text to send'), + threadTs: z + .string() + .optional() + .describe('Timestamp of another message to reply to (creates a threaded reply)'), + unfurlLinks: z + .boolean() + .optional() + .describe('Whether to enable unfurling of primarily text-based content'), + unfurlMedia: z.boolean().optional().describe('Whether to enable unfurling of media content'), + }) +); export type SlackSendMessageInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/snowflake/snowflake.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/snowflake/snowflake.ts index 129d2af349c58..3219ca11c4f07 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/snowflake/snowflake.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/snowflake/snowflake.ts @@ -35,7 +35,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ActionContext, ConnectorSpec } from '../../connector_spec'; import type { ExecuteStatementInput, @@ -261,83 +261,91 @@ export const Snowflake: ConnectorSpec = { }, }, - schema: z.object({ - accountUrl: z - .url() - .transform((val) => val.replace(/\/+$/, '')) - .describe('Snowflake account URL') - .meta({ - widget: 'text', - placeholder: 'https://.snowflakecomputing.com', - label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.accountUrl.label', { - defaultMessage: 'Snowflake Account URL', + schema: lazySchema(() => + z.object({ + accountUrl: z + .url() + .transform((val) => val.replace(/\/+$/, '')) + .describe('Snowflake account URL') + .meta({ + widget: 'text', + placeholder: 'https://.snowflakecomputing.com', + label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.accountUrl.label', { + defaultMessage: 'Snowflake Account URL', + }), + helpText: i18n.translate( + 'core.kibanaConnectorSpecs.snowflake.config.accountUrl.helpText', + { + defaultMessage: + 'The base URL for your Snowflake account (e.g. https://myorg-myaccount.snowflakecomputing.com).', + } + ), }), - helpText: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.accountUrl.helpText', { - defaultMessage: - 'The base URL for your Snowflake account (e.g. https://myorg-myaccount.snowflakecomputing.com).', + warehouse: z + .string() + .optional() + .describe('Default warehouse for SQL execution') + .meta({ + widget: 'text', + placeholder: 'COMPUTE_WH', + label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.warehouse.label', { + defaultMessage: 'Default Warehouse', + }), + helpText: i18n.translate( + 'core.kibanaConnectorSpecs.snowflake.config.warehouse.helpText', + { + defaultMessage: + 'Default warehouse to use when executing statements. Case-sensitive. Can be overridden per request.', + } + ), }), - }), - warehouse: z - .string() - .optional() - .describe('Default warehouse for SQL execution') - .meta({ - widget: 'text', - placeholder: 'COMPUTE_WH', - label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.warehouse.label', { - defaultMessage: 'Default Warehouse', + database: z + .string() + .optional() + .describe('Default database for SQL execution') + .meta({ + widget: 'text', + placeholder: 'MY_DATABASE', + label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.database.label', { + defaultMessage: 'Default Database', + }), + helpText: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.database.helpText', { + defaultMessage: + 'Default database to use when executing statements. Case-sensitive. Can be overridden per request.', + }), }), - helpText: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.warehouse.helpText', { - defaultMessage: - 'Default warehouse to use when executing statements. Case-sensitive. Can be overridden per request.', + defaultSchema: z + .string() + .optional() + .describe('Default schema for SQL execution') + .meta({ + widget: 'text', + placeholder: 'PUBLIC', + label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.schema.label', { + defaultMessage: 'Default Schema', + }), + helpText: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.schema.helpText', { + defaultMessage: + 'Default schema to use when executing statements. Case-sensitive. Can be overridden per request.', + }), }), - }), - database: z - .string() - .optional() - .describe('Default database for SQL execution') - .meta({ - widget: 'text', - placeholder: 'MY_DATABASE', - label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.database.label', { - defaultMessage: 'Default Database', + role: z + .string() + .optional() + .describe('Default role for SQL execution') + .meta({ + widget: 'text', + placeholder: 'PUBLIC', + label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.role.label', { + defaultMessage: 'Default Role', + }), + helpText: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.role.helpText', { + defaultMessage: + 'Default role to use when executing statements. Case-sensitive. Can be overridden per request.', + }), }), - helpText: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.database.helpText', { - defaultMessage: - 'Default database to use when executing statements. Case-sensitive. Can be overridden per request.', - }), - }), - defaultSchema: z - .string() - .optional() - .describe('Default schema for SQL execution') - .meta({ - widget: 'text', - placeholder: 'PUBLIC', - label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.schema.label', { - defaultMessage: 'Default Schema', - }), - helpText: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.schema.helpText', { - defaultMessage: - 'Default schema to use when executing statements. Case-sensitive. Can be overridden per request.', - }), - }), - role: z - .string() - .optional() - .describe('Default role for SQL execution') - .meta({ - widget: 'text', - placeholder: 'PUBLIC', - label: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.role.label', { - defaultMessage: 'Default Role', - }), - helpText: i18n.translate('core.kibanaConnectorSpecs.snowflake.config.role.helpText', { - defaultMessage: - 'Default role to use when executing statements. Case-sensitive. Can be overridden per request.', - }), - }), - }), + }) + ), validateUrls: { fields: ['accountUrl'], diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/snowflake/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/snowflake/types.ts index c3db3f77dd394..87cd56c325feb 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/snowflake/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/snowflake/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // --------------------------------------------------------------------------- // Snowflake SQL API binding types @@ -28,179 +28,189 @@ const SNOWFLAKE_BINDING_TYPES = [ 'TIMESTAMP_NTZ', ] as const; -const BindingValueSchema = z.object({ - type: z - .enum(SNOWFLAKE_BINDING_TYPES) - .describe( - 'Snowflake data type for the bind variable. Common types: TEXT for strings, FIXED for integers, REAL for floats, BOOLEAN for booleans, DATE/TIMESTAMP_* for temporal values.' - ), - value: z - .string() - .describe( - 'String representation of the value. All values must be strings, e.g. "123" for integer 123, "true" for boolean true.' - ), -}); +const BindingValueSchema = lazySchema(() => + z.object({ + type: z + .enum(SNOWFLAKE_BINDING_TYPES) + .describe( + 'Snowflake data type for the bind variable. Common types: TEXT for strings, FIXED for integers, REAL for floats, BOOLEAN for booleans, DATE/TIMESTAMP_* for temporal values.' + ), + value: z + .string() + .describe( + 'String representation of the value. All values must be strings, e.g. "123" for integer 123, "true" for boolean true.' + ), + }) +); // --------------------------------------------------------------------------- // executeStatement // --------------------------------------------------------------------------- -export const ExecuteStatementInputSchema = z.object({ - statement: z - .string() - .min(1) - .describe( - 'SQL statement to execute. Supports any Snowflake SQL including SELECT, INSERT, UPDATE, DELETE, CREATE, etc. Use "?" placeholders for bind variables and provide values via the bindings parameter. Multiple statements can be separated by semicolons when multiStatementCount is set.' - ), - timeout: z - .number() - .int() - .min(0) - .max(604800) - .optional() - .describe( - 'Timeout in seconds for statement execution (0–604800). 0 sets the maximum timeout (7 days). If omitted, uses the STATEMENT_TIMEOUT_IN_SECONDS session parameter.' - ), - database: z - .string() - .optional() - .describe( - 'Database to use for execution. Case-sensitive — must match the value returned by SHOW DATABASES. If omitted, uses the user default (DEFAULT_NAMESPACE).' - ), - schema: z - .string() - .optional() - .describe( - 'Schema to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_NAMESPACE).' - ), - warehouse: z - .string() - .optional() - .describe( - 'Warehouse to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_WAREHOUSE).' - ), - role: z - .string() - .optional() - .describe( - 'Role to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_ROLE).' - ), - bindings: z - .record(z.string(), BindingValueSchema) - .optional() - .describe( - 'Bind variable values keyed by 1-based position (e.g. {"1": {"type": "FIXED", "value": "123"}}). Each key corresponds to a "?" placeholder in the SQL statement.' - ), - multiStatementCount: z - .number() - .int() - .min(0) - .optional() - .describe( - 'Number of SQL statements in the request when using multi-statement execution. Set to 0 for a variable number, or the exact count. Required when submitting more than one statement separated by semicolons.' - ), - queryTag: z - .string() - .optional() - .describe( - 'Tag to associate with the query for tracking and filtering in Snowflake query history.' - ), -}); +export const ExecuteStatementInputSchema = lazySchema(() => + z.object({ + statement: z + .string() + .min(1) + .describe( + 'SQL statement to execute. Supports any Snowflake SQL including SELECT, INSERT, UPDATE, DELETE, CREATE, etc. Use "?" placeholders for bind variables and provide values via the bindings parameter. Multiple statements can be separated by semicolons when multiStatementCount is set.' + ), + timeout: z + .number() + .int() + .min(0) + .max(604800) + .optional() + .describe( + 'Timeout in seconds for statement execution (0–604800). 0 sets the maximum timeout (7 days). If omitted, uses the STATEMENT_TIMEOUT_IN_SECONDS session parameter.' + ), + database: z + .string() + .optional() + .describe( + 'Database to use for execution. Case-sensitive — must match the value returned by SHOW DATABASES. If omitted, uses the user default (DEFAULT_NAMESPACE).' + ), + schema: z + .string() + .optional() + .describe( + 'Schema to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_NAMESPACE).' + ), + warehouse: z + .string() + .optional() + .describe( + 'Warehouse to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_WAREHOUSE).' + ), + role: z + .string() + .optional() + .describe( + 'Role to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_ROLE).' + ), + bindings: z + .record(z.string(), BindingValueSchema) + .optional() + .describe( + 'Bind variable values keyed by 1-based position (e.g. {"1": {"type": "FIXED", "value": "123"}}). Each key corresponds to a "?" placeholder in the SQL statement.' + ), + multiStatementCount: z + .number() + .int() + .min(0) + .optional() + .describe( + 'Number of SQL statements in the request when using multi-statement execution. Set to 0 for a variable number, or the exact count. Required when submitting more than one statement separated by semicolons.' + ), + queryTag: z + .string() + .optional() + .describe( + 'Tag to associate with the query for tracking and filtering in Snowflake query history.' + ), + }) +); export type ExecuteStatementInput = z.infer; // --------------------------------------------------------------------------- // runQuery // --------------------------------------------------------------------------- -export const RunQueryInputSchema = z.object({ - statement: z - .string() - .min(1) - .describe( - 'Read-only SQL statement to run. Only SELECT, WITH (CTE), SHOW, DESCRIBE / DESC, and EXPLAIN are accepted. Write operations (INSERT, UPDATE, DELETE, MERGE), DDL (CREATE, ALTER, DROP, TRUNCATE), privilege changes (GRANT, REVOKE), stored procedure calls (CALL), and session state changes (USE, SET) are rejected. Use "?" placeholders for bind variables and provide values via the bindings parameter. Single-statement only — semicolon-delimited multi-statement submissions are rejected.' - ), - timeout: z - .number() - .int() - .min(0) - .max(604800) - .optional() - .describe( - 'Timeout in seconds for statement execution (0–604800). 0 sets the maximum timeout (7 days). If omitted, uses the STATEMENT_TIMEOUT_IN_SECONDS session parameter.' - ), - database: z - .string() - .optional() - .describe( - 'Database to use for execution. Case-sensitive — must match the value returned by SHOW DATABASES. If omitted, uses the user default (DEFAULT_NAMESPACE).' - ), - schema: z - .string() - .optional() - .describe( - 'Schema to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_NAMESPACE).' - ), - warehouse: z - .string() - .optional() - .describe( - 'Warehouse to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_WAREHOUSE).' - ), - role: z - .string() - .optional() - .describe( - 'Role to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_ROLE).' - ), - bindings: z - .record(z.string(), BindingValueSchema) - .optional() - .describe( - 'Bind variable values keyed by 1-based position (e.g. {"1": {"type": "FIXED", "value": "123"}}). Each key corresponds to a "?" placeholder in the SQL statement.' - ), - queryTag: z - .string() - .optional() - .describe( - 'Tag to associate with the query for tracking and filtering in Snowflake query history.' - ), -}); +export const RunQueryInputSchema = lazySchema(() => + z.object({ + statement: z + .string() + .min(1) + .describe( + 'Read-only SQL statement to run. Only SELECT, WITH (CTE), SHOW, DESCRIBE / DESC, and EXPLAIN are accepted. Write operations (INSERT, UPDATE, DELETE, MERGE), DDL (CREATE, ALTER, DROP, TRUNCATE), privilege changes (GRANT, REVOKE), stored procedure calls (CALL), and session state changes (USE, SET) are rejected. Use "?" placeholders for bind variables and provide values via the bindings parameter. Single-statement only — semicolon-delimited multi-statement submissions are rejected.' + ), + timeout: z + .number() + .int() + .min(0) + .max(604800) + .optional() + .describe( + 'Timeout in seconds for statement execution (0–604800). 0 sets the maximum timeout (7 days). If omitted, uses the STATEMENT_TIMEOUT_IN_SECONDS session parameter.' + ), + database: z + .string() + .optional() + .describe( + 'Database to use for execution. Case-sensitive — must match the value returned by SHOW DATABASES. If omitted, uses the user default (DEFAULT_NAMESPACE).' + ), + schema: z + .string() + .optional() + .describe( + 'Schema to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_NAMESPACE).' + ), + warehouse: z + .string() + .optional() + .describe( + 'Warehouse to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_WAREHOUSE).' + ), + role: z + .string() + .optional() + .describe( + 'Role to use for execution. Case-sensitive. If omitted, uses the user default (DEFAULT_ROLE).' + ), + bindings: z + .record(z.string(), BindingValueSchema) + .optional() + .describe( + 'Bind variable values keyed by 1-based position (e.g. {"1": {"type": "FIXED", "value": "123"}}). Each key corresponds to a "?" placeholder in the SQL statement.' + ), + queryTag: z + .string() + .optional() + .describe( + 'Tag to associate with the query for tracking and filtering in Snowflake query history.' + ), + }) +); export type RunQueryInput = z.infer; // --------------------------------------------------------------------------- // getStatementStatus // --------------------------------------------------------------------------- -export const GetStatementStatusInputSchema = z.object({ - statementHandle: z - .string() - .min(1) - .describe( - 'The statement handle (UUID) returned by executeStatement. Used to poll for results or check execution progress.' - ), - partition: z - .number() - .int() - .min(0) - .optional() - .describe( - 'Partition number to retrieve (0-based). Snowflake splits large result sets into partitions. Omit to get the first partition.' - ), -}); +export const GetStatementStatusInputSchema = lazySchema(() => + z.object({ + statementHandle: z + .string() + .min(1) + .describe( + 'The statement handle (UUID) returned by executeStatement. Used to poll for results or check execution progress.' + ), + partition: z + .number() + .int() + .min(0) + .optional() + .describe( + 'Partition number to retrieve (0-based). Snowflake splits large result sets into partitions. Omit to get the first partition.' + ), + }) +); export type GetStatementStatusInput = z.infer; // --------------------------------------------------------------------------- // cancelStatement // --------------------------------------------------------------------------- -export const CancelStatementInputSchema = z.object({ - statementHandle: z - .string() - .min(1) - .describe( - 'The statement handle (UUID) of the running statement to cancel. Obtain this from the executeStatement response.' - ), -}); +export const CancelStatementInputSchema = lazySchema(() => + z.object({ + statementHandle: z + .string() + .min(1) + .describe( + 'The statement handle (UUID) of the running statement to cancel. Obtain this from the executeStatement response.' + ), + }) +); export type CancelStatementInput = z.infer; // --------------------------------------------------------------------------- @@ -208,158 +218,174 @@ export type CancelStatementInput = z.infer; // https://docs.snowflake.com/en/developer-guide/snowflake-rest-api/reference // --------------------------------------------------------------------------- -export const ListCommonQueryParamsSchema = z.object({ - like: z - .string() - .optional() - .describe( - 'Case-insensitive SQL pattern to filter by object name. Supports "%" (any sequence) and "_" (single char) wildcards. Examples: "CUST%", "%_LOG", "ORDERS". Omit to return all visible objects.' - ), - startsWith: z - .string() - .optional() - .describe( - 'Case-sensitive prefix filter on the object name. Unlike like, this does not use wildcards. Example: "PROD_" returns only names starting with exactly "PROD_".' - ), - showLimit: z - .number() - .int() - .min(1) - .max(10000) - .optional() - .describe( - 'Maximum number of rows to return (1–10000). Prefer <=100 to keep LLM context small. When omitted, Snowflake applies a server-side default.' - ), - fromName: z - .string() - .optional() - .describe( - 'Cursor for pagination. Returns only rows whose name sorts after this value (case-sensitive, alphabetical). Use the last name from a previous page to fetch the next page.' - ), -}); +export const ListCommonQueryParamsSchema = lazySchema(() => + z.object({ + like: z + .string() + .optional() + .describe( + 'Case-insensitive SQL pattern to filter by object name. Supports "%" (any sequence) and "_" (single char) wildcards. Examples: "CUST%", "%_LOG", "ORDERS". Omit to return all visible objects.' + ), + startsWith: z + .string() + .optional() + .describe( + 'Case-sensitive prefix filter on the object name. Unlike like, this does not use wildcards. Example: "PROD_" returns only names starting with exactly "PROD_".' + ), + showLimit: z + .number() + .int() + .min(1) + .max(10000) + .optional() + .describe( + 'Maximum number of rows to return (1–10000). Prefer <=100 to keep LLM context small. When omitted, Snowflake applies a server-side default.' + ), + fromName: z + .string() + .optional() + .describe( + 'Cursor for pagination. Returns only rows whose name sorts after this value (case-sensitive, alphabetical). Use the last name from a previous page to fetch the next page.' + ), + }) +); // --------------------------------------------------------------------------- // listDatabases // --------------------------------------------------------------------------- -export const ListDatabasesInputSchema = ListCommonQueryParamsSchema.extend({ - history: z - .boolean() - .optional() - .describe( - 'If true, include dropped databases that have not yet been purged. Defaults to false.' - ), -}); +export const ListDatabasesInputSchema = lazySchema(() => + ListCommonQueryParamsSchema.extend({ + history: z + .boolean() + .optional() + .describe( + 'If true, include dropped databases that have not yet been purged. Defaults to false.' + ), + }) +); export type ListDatabasesInput = z.infer; // --------------------------------------------------------------------------- // listSchemas // --------------------------------------------------------------------------- -export const ListSchemasInputSchema = ListCommonQueryParamsSchema.extend({ - database: z - .string() - .min(1) - .describe( - 'Case-sensitive database name (e.g. "PROD_DB") whose schemas to list. Use listDatabases to discover available databases.' - ), -}); +export const ListSchemasInputSchema = lazySchema(() => + ListCommonQueryParamsSchema.extend({ + database: z + .string() + .min(1) + .describe( + 'Case-sensitive database name (e.g. "PROD_DB") whose schemas to list. Use listDatabases to discover available databases.' + ), + }) +); export type ListSchemasInput = z.infer; // --------------------------------------------------------------------------- // listTables // --------------------------------------------------------------------------- -export const ListTablesInputSchema = ListCommonQueryParamsSchema.extend({ - database: z - .string() - .min(1) - .describe( - 'Case-sensitive database name containing the schema. Use listDatabases to discover available databases.' - ), - schema: z - .string() - .min(1) - .describe( - 'Case-sensitive schema name whose tables to list (e.g. "PUBLIC"). Use listSchemas to discover available schemas.' - ), - history: z - .boolean() - .optional() - .describe('If true, include dropped tables that have not yet been purged. Defaults to false.'), -}); +export const ListTablesInputSchema = lazySchema(() => + ListCommonQueryParamsSchema.extend({ + database: z + .string() + .min(1) + .describe( + 'Case-sensitive database name containing the schema. Use listDatabases to discover available databases.' + ), + schema: z + .string() + .min(1) + .describe( + 'Case-sensitive schema name whose tables to list (e.g. "PUBLIC"). Use listSchemas to discover available schemas.' + ), + history: z + .boolean() + .optional() + .describe( + 'If true, include dropped tables that have not yet been purged. Defaults to false.' + ), + }) +); export type ListTablesInput = z.infer; // --------------------------------------------------------------------------- // listViews // --------------------------------------------------------------------------- -export const ListViewsInputSchema = ListCommonQueryParamsSchema.extend({ - database: z - .string() - .min(1) - .describe( - 'Case-sensitive database name containing the schema. Use listDatabases to discover available databases.' - ), - schema: z - .string() - .min(1) - .describe( - 'Case-sensitive schema name whose views to list (e.g. "PUBLIC"). Use listSchemas to discover available schemas.' - ), -}); +export const ListViewsInputSchema = lazySchema(() => + ListCommonQueryParamsSchema.extend({ + database: z + .string() + .min(1) + .describe( + 'Case-sensitive database name containing the schema. Use listDatabases to discover available databases.' + ), + schema: z + .string() + .min(1) + .describe( + 'Case-sensitive schema name whose views to list (e.g. "PUBLIC"). Use listSchemas to discover available schemas.' + ), + }) +); export type ListViewsInput = z.infer; // --------------------------------------------------------------------------- // describeTable // --------------------------------------------------------------------------- -export const DescribeTableInputSchema = z.object({ - database: z - .string() - .min(1) - .describe( - 'Case-sensitive database name containing the table (e.g. "PROD_DB"). Must match the value returned by listDatabases.' - ), - schema: z - .string() - .min(1) - .describe( - 'Case-sensitive schema name containing the table (e.g. "PUBLIC"). Must match the value returned by listSchemas.' - ), - name: z - .string() - .min(1) - .describe( - 'Case-sensitive table name (e.g. "ORDERS"). Must match the value returned by listTables. Returns columns (name, type, nullable, default, comment), clustering keys, row count, and other metadata.' - ), -}); +export const DescribeTableInputSchema = lazySchema(() => + z.object({ + database: z + .string() + .min(1) + .describe( + 'Case-sensitive database name containing the table (e.g. "PROD_DB"). Must match the value returned by listDatabases.' + ), + schema: z + .string() + .min(1) + .describe( + 'Case-sensitive schema name containing the table (e.g. "PUBLIC"). Must match the value returned by listSchemas.' + ), + name: z + .string() + .min(1) + .describe( + 'Case-sensitive table name (e.g. "ORDERS"). Must match the value returned by listTables. Returns columns (name, type, nullable, default, comment), clustering keys, row count, and other metadata.' + ), + }) +); export type DescribeTableInput = z.infer; // --------------------------------------------------------------------------- // describeView // --------------------------------------------------------------------------- -export const DescribeViewInputSchema = z.object({ - database: z - .string() - .min(1) - .describe( - 'Case-sensitive database name containing the view. Must match the value returned by listDatabases.' - ), - schema: z - .string() - .min(1) - .describe( - 'Case-sensitive schema name containing the view. Must match the value returned by listSchemas.' - ), - name: z - .string() - .min(1) - .describe( - 'Case-sensitive view name. Must match the value returned by listViews. Returns the view definition, columns, and the underlying query text.' - ), -}); +export const DescribeViewInputSchema = lazySchema(() => + z.object({ + database: z + .string() + .min(1) + .describe( + 'Case-sensitive database name containing the view. Must match the value returned by listDatabases.' + ), + schema: z + .string() + .min(1) + .describe( + 'Case-sensitive schema name containing the view. Must match the value returned by listSchemas.' + ), + name: z + .string() + .min(1) + .describe( + 'Case-sensitive view name. Must match the value returned by listViews. Returns the view definition, columns, and the underlying query text.' + ), + }) +); export type DescribeViewInput = z.infer; // --------------------------------------------------------------------------- @@ -367,69 +393,73 @@ export type DescribeViewInput = z.infer; // https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-search/query-cortex-search-service // --------------------------------------------------------------------------- -export const ListCortexSearchServicesInputSchema = ListCommonQueryParamsSchema.omit({ - startsWith: true, -}).extend({ - database: z - .string() - .min(1) - .describe( - 'Case-sensitive database name containing the schema. Use listDatabases to discover available databases.' - ), - schema: z - .string() - .min(1) - .describe( - 'Case-sensitive schema name whose Cortex Search services to list. Use listSchemas to discover available schemas.' - ), -}); +export const ListCortexSearchServicesInputSchema = lazySchema(() => + ListCommonQueryParamsSchema.omit({ + startsWith: true, + }).extend({ + database: z + .string() + .min(1) + .describe( + 'Case-sensitive database name containing the schema. Use listDatabases to discover available databases.' + ), + schema: z + .string() + .min(1) + .describe( + 'Case-sensitive schema name whose Cortex Search services to list. Use listSchemas to discover available schemas.' + ), + }) +); export type ListCortexSearchServicesInput = z.infer; -export const CortexSearchInputSchema = z.object({ - database: z - .string() - .min(1) - .describe( - 'Case-sensitive database name containing the Cortex Search service. Use listDatabases or listCortexSearchServices to discover.' - ), - schema: z - .string() - .min(1) - .describe( - 'Case-sensitive schema name containing the Cortex Search service. Use listCortexSearchServices to discover.' - ), - serviceName: z - .string() - .min(1) - .describe( - 'Case-sensitive name of the Cortex Search service to query. Use listCortexSearchServices to discover available services.' - ), - query: z - .string() - .min(1) - .describe( - "Natural-language search query to run against the service's indexed search column. Cortex Search performs semantic + lexical matching automatically." - ), - columns: z - .array(z.string()) - .optional() - .describe( - "Additional columns to return for each result. Must be included in the service's source query. If omitted, only the indexed search column is returned." - ), - filter: z - .record(z.string(), z.unknown()) - .optional() - .describe( - 'Filter object restricting results by ATTRIBUTES columns. Supported operators: @eq (text/numeric equality), @contains (array membership), @gte/@lte (numeric/date range), @and, @or, @not. Examples: {"@eq": {"REGION": "US"}}; {"@and": [{"@gte": {"LIKES": 50}}, {"@contains": {"TAGS": "ai"}}]}.' - ), - limit: z - .number() - .int() - .min(1) - .max(1000) - .optional() - .describe( - 'Maximum number of results to return (1–1000). Defaults to 10. Prefer <=20 to keep LLM context small.' - ), -}); +export const CortexSearchInputSchema = lazySchema(() => + z.object({ + database: z + .string() + .min(1) + .describe( + 'Case-sensitive database name containing the Cortex Search service. Use listDatabases or listCortexSearchServices to discover.' + ), + schema: z + .string() + .min(1) + .describe( + 'Case-sensitive schema name containing the Cortex Search service. Use listCortexSearchServices to discover.' + ), + serviceName: z + .string() + .min(1) + .describe( + 'Case-sensitive name of the Cortex Search service to query. Use listCortexSearchServices to discover available services.' + ), + query: z + .string() + .min(1) + .describe( + "Natural-language search query to run against the service's indexed search column. Cortex Search performs semantic + lexical matching automatically." + ), + columns: z + .array(z.string()) + .optional() + .describe( + "Additional columns to return for each result. Must be included in the service's source query. If omitted, only the indexed search column is returned." + ), + filter: z + .record(z.string(), z.unknown()) + .optional() + .describe( + 'Filter object restricting results by ATTRIBUTES columns. Supported operators: @eq (text/numeric equality), @contains (array membership), @gte/@lte (numeric/date range), @and, @or, @not. Examples: {"@eq": {"REGION": "US"}}; {"@and": [{"@gte": {"LIKES": 50}}, {"@contains": {"TAGS": "ai"}}]}.' + ), + limit: z + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe( + 'Maximum number of results to return (1–1000). Defaults to 10. Prefer <=20 to keep LLM context small.' + ), + }) +); export type CortexSearchInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/tavily/tavily.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/tavily/tavily.ts index 6a4a4bf25677a..7de9b15dfd512 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/tavily/tavily.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/tavily/tavily.ts @@ -16,7 +16,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { UISchemas, type ConnectorSpec } from '../../connector_spec'; import { withMcpClient, callToolContent, callToolJson } from '../../lib/mcp'; import type { CallToolInput, CrawlInput, ExtractInput, MapInput, SearchInput } from './types'; @@ -47,21 +47,23 @@ export const TavilyConnector: ConnectorSpec = { types: ['bearer'], }, - schema: z.object({ - serverUrl: UISchemas.url() - .default(TAVILY_MCP_SERVER_URL) - .describe('Tavily MCP Server URL') - .meta({ - widget: 'text', - placeholder: 'https://mcp.tavily.com/mcp/', - label: i18n.translate('connectorSpecs.tavily.config.serverUrl.label', { - defaultMessage: 'MCP Server URL', + schema: lazySchema(() => + z.object({ + serverUrl: UISchemas.url() + .default(TAVILY_MCP_SERVER_URL) + .describe('Tavily MCP Server URL') + .meta({ + widget: 'text', + placeholder: 'https://mcp.tavily.com/mcp/', + label: i18n.translate('connectorSpecs.tavily.config.serverUrl.label', { + defaultMessage: 'MCP Server URL', + }), + helpText: i18n.translate('connectorSpecs.tavily.config.serverUrl.helpText', { + defaultMessage: 'The URL of the Tavily MCP server.', + }), }), - helpText: i18n.translate('connectorSpecs.tavily.config.serverUrl.helpText', { - defaultMessage: 'The URL of the Tavily MCP server.', - }), - }), - }), + }) + ), validateUrls: { fields: ['serverUrl'], diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/tavily/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/tavily/types.ts index ba010e4cac8cf..a5d6b2a8634cb 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/tavily/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/tavily/types.ts @@ -7,163 +7,173 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // ============================================================================= // Action input schemas & inferred types // ============================================================================= -export const ListToolsInputSchema = z.object({}); +export const ListToolsInputSchema = lazySchema(() => z.object({})); export type ListToolsInput = z.infer; -export const SearchInputSchema = z.object({ - query: z - .string() - .min(1) - .describe( - 'The search query to execute. Example: "latest AI research 2025" or "how to configure Elasticsearch index mappings".' - ), - max_results: z - .number() - .optional() - .default(10) - .describe( - 'Maximum number of search results to return. Defaults to 10. Increase for broader coverage; decrease for faster, more focused results.' - ), - search_depth: z - .enum(['basic', 'advanced', 'fast', 'ultra-fast']) - .optional() - .default('basic') - .describe( - 'Search depth controls thoroughness vs. latency. Use "basic" (default) for general queries, "advanced" for in-depth research requiring comprehensive results, "fast" or "ultra-fast" when low latency is critical and some result quality can be sacrificed.' - ), - include_raw_content: z - .boolean() - .optional() - .default(false) - .describe( - 'When true, includes the full cleaned and parsed HTML content of each search result page. Significantly increases response size — only use when you need the full page text rather than the snippet summary.' - ), -}); +export const SearchInputSchema = lazySchema(() => + z.object({ + query: z + .string() + .min(1) + .describe( + 'The search query to execute. Example: "latest AI research 2025" or "how to configure Elasticsearch index mappings".' + ), + max_results: z + .number() + .optional() + .default(10) + .describe( + 'Maximum number of search results to return. Defaults to 10. Increase for broader coverage; decrease for faster, more focused results.' + ), + search_depth: z + .enum(['basic', 'advanced', 'fast', 'ultra-fast']) + .optional() + .default('basic') + .describe( + 'Search depth controls thoroughness vs. latency. Use "basic" (default) for general queries, "advanced" for in-depth research requiring comprehensive results, "fast" or "ultra-fast" when low latency is critical and some result quality can be sacrificed.' + ), + include_raw_content: z + .boolean() + .optional() + .default(false) + .describe( + 'When true, includes the full cleaned and parsed HTML content of each search result page. Significantly increases response size — only use when you need the full page text rather than the snippet summary.' + ), + }) +); export type SearchInput = z.infer; -export const ExtractInputSchema = z.object({ - urls: z - .array(z.string()) - .min(1) - .describe( - 'List of one or more URLs to extract content from. Example: ["https://example.com/article", "https://docs.elastic.co/guide"]. At least one URL is required.' - ), - extract_depth: z - .enum(['basic', 'advanced']) - .optional() - .default('basic') - .describe( - 'Depth of extraction. "basic" (default) works for most public web pages. Use "advanced" for LinkedIn profiles, paywalled or protected sites, pages with tables, or pages with embedded/dynamic content that basic extraction misses.' - ), - include_images: z - .boolean() - .optional() - .default(false) - .describe( - 'When true, includes image URLs extracted from the pages. Defaults to false. Enable when visual content such as diagrams or screenshots is relevant to the task.' - ), -}); +export const ExtractInputSchema = lazySchema(() => + z.object({ + urls: z + .array(z.string()) + .min(1) + .describe( + 'List of one or more URLs to extract content from. Example: ["https://example.com/article", "https://docs.elastic.co/guide"]. At least one URL is required.' + ), + extract_depth: z + .enum(['basic', 'advanced']) + .optional() + .default('basic') + .describe( + 'Depth of extraction. "basic" (default) works for most public web pages. Use "advanced" for LinkedIn profiles, paywalled or protected sites, pages with tables, or pages with embedded/dynamic content that basic extraction misses.' + ), + include_images: z + .boolean() + .optional() + .default(false) + .describe( + 'When true, includes image URLs extracted from the pages. Defaults to false. Enable when visual content such as diagrams or screenshots is relevant to the task.' + ), + }) +); export type ExtractInput = z.infer; -export const CrawlInputSchema = z.object({ - url: z - .string() - .min(1) - .describe( - 'The root URL to begin the crawl. The crawler will start here and follow links outward. Example: "https://docs.elastic.co/kibana".' - ), - max_depth: z - .number() - .optional() - .default(1) - .describe( - 'Maximum depth of the crawl tree from the root URL. Depth 1 means only pages directly linked from the root; depth 2 includes pages linked from those; and so on. Defaults to 1. Higher values increase coverage but also time and cost.' - ), - max_breadth: z - .number() - .optional() - .default(20) - .describe( - 'Maximum number of links to follow per page (per level of the tree). Defaults to 20. Lower values narrow the crawl to the most prominent links on each page.' - ), - limit: z - .number() - .optional() - .default(50) - .describe( - 'Total number of pages the crawler will process before stopping, regardless of depth or breadth settings. Defaults to 50. Prefer low limits (5-20 pages) to avoid overwhelming the context window. Acts as a hard cap to control cost and response size.' - ), - instructions: z - .string() - .optional() - .describe( - 'Optional natural language instructions guiding which types of pages to include or exclude. Example: "Only return pages about API reference documentation" or "Skip blog posts and focus on product pages."' - ), - extract_depth: z - .enum(['basic', 'advanced']) - .optional() - .default('basic') - .describe( - 'Depth of content extraction per crawled page. "basic" (default) works for most pages. Use "advanced" to retrieve richer data including tables and embedded content.' - ), -}); +export const CrawlInputSchema = lazySchema(() => + z.object({ + url: z + .string() + .min(1) + .describe( + 'The root URL to begin the crawl. The crawler will start here and follow links outward. Example: "https://docs.elastic.co/kibana".' + ), + max_depth: z + .number() + .optional() + .default(1) + .describe( + 'Maximum depth of the crawl tree from the root URL. Depth 1 means only pages directly linked from the root; depth 2 includes pages linked from those; and so on. Defaults to 1. Higher values increase coverage but also time and cost.' + ), + max_breadth: z + .number() + .optional() + .default(20) + .describe( + 'Maximum number of links to follow per page (per level of the tree). Defaults to 20. Lower values narrow the crawl to the most prominent links on each page.' + ), + limit: z + .number() + .optional() + .default(50) + .describe( + 'Total number of pages the crawler will process before stopping, regardless of depth or breadth settings. Defaults to 50. Prefer low limits (5-20 pages) to avoid overwhelming the context window. Acts as a hard cap to control cost and response size.' + ), + instructions: z + .string() + .optional() + .describe( + 'Optional natural language instructions guiding which types of pages to include or exclude. Example: "Only return pages about API reference documentation" or "Skip blog posts and focus on product pages."' + ), + extract_depth: z + .enum(['basic', 'advanced']) + .optional() + .default('basic') + .describe( + 'Depth of content extraction per crawled page. "basic" (default) works for most pages. Use "advanced" to retrieve richer data including tables and embedded content.' + ), + }) +); export type CrawlInput = z.infer; -export const MapInputSchema = z.object({ - url: z - .string() - .min(1) - .describe( - 'The root URL to begin the site mapping. The mapper will discover links starting here. Example: "https://docs.elastic.co/kibana".' - ), - max_depth: z - .number() - .optional() - .default(1) - .describe( - 'Maximum depth of link traversal from the root URL. Depth 1 returns only URLs directly linked from the root page; depth 2 adds URLs linked from those; and so on. Defaults to 1.' - ), - max_breadth: z - .number() - .optional() - .default(20) - .describe( - 'Maximum number of links to follow per page (per level of the tree). Defaults to 20. Lower values focus on the most prominent links on each page.' - ), - limit: z - .number() - .optional() - .default(50) - .describe( - 'Total number of URLs to discover before stopping. Defaults to 50. Acts as a hard cap regardless of depth or breadth settings.' - ), - instructions: z - .string() - .optional() - .describe( - 'Optional natural language instructions guiding which types of URLs to include or exclude during mapping. Example: "Only include URLs under the /api/ path" or "Skip any URLs containing /blog/".' - ), -}); +export const MapInputSchema = lazySchema(() => + z.object({ + url: z + .string() + .min(1) + .describe( + 'The root URL to begin the site mapping. The mapper will discover links starting here. Example: "https://docs.elastic.co/kibana".' + ), + max_depth: z + .number() + .optional() + .default(1) + .describe( + 'Maximum depth of link traversal from the root URL. Depth 1 returns only URLs directly linked from the root page; depth 2 adds URLs linked from those; and so on. Defaults to 1.' + ), + max_breadth: z + .number() + .optional() + .default(20) + .describe( + 'Maximum number of links to follow per page (per level of the tree). Defaults to 20. Lower values focus on the most prominent links on each page.' + ), + limit: z + .number() + .optional() + .default(50) + .describe( + 'Total number of URLs to discover before stopping. Defaults to 50. Acts as a hard cap regardless of depth or breadth settings.' + ), + instructions: z + .string() + .optional() + .describe( + 'Optional natural language instructions guiding which types of URLs to include or exclude during mapping. Example: "Only include URLs under the /api/ path" or "Skip any URLs containing /blog/".' + ), + }) +); export type MapInput = z.infer; -export const CallToolInputSchema = z.object({ - name: z - .string() - .min(1) - .describe( - 'Name of the MCP tool to call on the Tavily MCP server. Use the listTools action first to discover available tool names if you are unsure. Example: "tavily_search".' - ), - arguments: z - .record(z.string(), z.unknown()) - .optional() - .describe( - 'Arguments to pass to the tool as a key-value object. The required and optional keys depend on the specific tool being called. Use listTools to see each tool\'s parameter schema. Example: { "query": "AI news", "max_results": 5 }.' - ), -}); +export const CallToolInputSchema = lazySchema(() => + z.object({ + name: z + .string() + .min(1) + .describe( + 'Name of the MCP tool to call on the Tavily MCP server. Use the listTools action first to discover available tool names if you are unsure. Example: "tavily_search".' + ), + arguments: z + .record(z.string(), z.unknown()) + .optional() + .describe( + 'Arguments to pass to the tool as a key-value object. The required and optional keys depend on the specific tool being called. Use listTools to see each tool\'s parameter schema. Example: { "query": "AI news", "max_results": 5 }.' + ), + }) +); export type CallToolInput = z.infer; diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/urlvoid/urlvoid.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/urlvoid/urlvoid.ts index c3c60ca7dea21..eb5e90d31fa32 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/urlvoid/urlvoid.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/urlvoid/urlvoid.ts @@ -19,7 +19,7 @@ * MVP implementation focusing on core domain reputation actions. */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import { i18n } from '@kbn/i18n'; import type { ConnectorSpec } from '../../connector_spec'; @@ -42,9 +42,11 @@ export const URLVoidConnector: ConnectorSpec = { actions: { scanDomain: { isTool: true, - input: z.object({ - domain: z.string().describe('Domain name to scan'), - }), + input: lazySchema(() => + z.object({ + domain: z.string().describe('Domain name to scan'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { domain: string }; const apiKey = ctx.secrets?.authType === 'api_key_header' ? ctx.secrets['X-Api-Key'] : ''; @@ -62,9 +64,11 @@ export const URLVoidConnector: ConnectorSpec = { checkUrl: { isTool: true, - input: z.object({ - url: z.url().describe('URL to check'), - }), + input: lazySchema(() => + z.object({ + url: z.url().describe('URL to check'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { url: string }; const domain = new URL(typedInput.url).hostname; @@ -84,9 +88,11 @@ export const URLVoidConnector: ConnectorSpec = { getDomainInfo: { isTool: true, - input: z.object({ - domain: z.string().describe('Domain name'), - }), + input: lazySchema(() => + z.object({ + domain: z.string().describe('Domain name'), + }) + ), handler: async (ctx, input) => { const typedInput = input as { domain: string }; const apiKey = ctx.secrets?.authType === 'api_key_header' ? ctx.secrets['X-Api-Key'] : ''; @@ -107,7 +113,7 @@ export const URLVoidConnector: ConnectorSpec = { scanDomainStats: { isTool: true, - input: z.object({}), + input: lazySchema(() => z.object({})), handler: async (ctx) => { const apiKey = ctx.secrets?.authType === 'api_key_header' ? ctx.secrets['X-Api-Key'] : ''; const response = await ctx.client.get( diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/virustotal/virustotal.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/virustotal/virustotal.ts index c01c38abf1ac3..3faa385cf72ef 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/virustotal/virustotal.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/virustotal/virustotal.ts @@ -21,7 +21,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; const VIRUSTOTAL_API_BASE_URL = 'https://www.virustotal.com/api/v3'; @@ -166,16 +166,18 @@ export const VirusTotalConnector: ConnectorSpec = { actions: { scanFileHash: { isTool: true, - input: z.object({ - hash: z.string().min(32).describe('File hash (MD5, SHA-1, or SHA-256)'), - failOnError: z - .boolean() - .optional() - .default(false) - .describe( - 'If true, throw error on API failures. If false (default), return error details' - ), - }), + input: lazySchema(() => + z.object({ + hash: z.string().min(32).describe('File hash (MD5, SHA-1, or SHA-256)'), + failOnError: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, throw error on API failures. If false (default), return error details' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { hash: string; failOnError?: boolean }; try { @@ -199,16 +201,18 @@ export const VirusTotalConnector: ConnectorSpec = { scanUrl: { isTool: true, - input: z.object({ - url: urlOrDomainSchema.describe('Absolute URL to scan, or bare domain to look up'), - failOnError: z - .boolean() - .optional() - .default(false) - .describe( - 'If true, throw error on API failures. If false (default), return error details' - ), - }), + input: lazySchema(() => + z.object({ + url: urlOrDomainSchema.describe('Absolute URL to scan, or bare domain to look up'), + failOnError: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, throw error on API failures. If false (default), return error details' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { url: string; failOnError?: boolean }; try { @@ -309,17 +313,19 @@ export const VirusTotalConnector: ConnectorSpec = { submitFile: { isTool: true, - input: z.object({ - file: z.string().describe('Base64-encoded file content'), - filename: z.string().optional().describe('Original filename'), - failOnError: z - .boolean() - .optional() - .default(false) - .describe( - 'If true, throw error on API failures. If false (default), return error details' - ), - }), + input: lazySchema(() => + z.object({ + file: z.string().describe('Base64-encoded file content'), + filename: z.string().optional().describe('Original filename'), + failOnError: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, throw error on API failures. If false (default), return error details' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { file: string; filename?: string; failOnError?: boolean }; try { @@ -345,16 +351,18 @@ export const VirusTotalConnector: ConnectorSpec = { getIpReport: { isTool: true, - input: z.object({ - ip: z.ipv4().describe('IP address'), - failOnError: z - .boolean() - .optional() - .default(false) - .describe( - 'If true, throw error on API failures. If false (default), return error details' - ), - }), + input: lazySchema(() => + z.object({ + ip: z.ipv4().describe('IP address'), + failOnError: z + .boolean() + .optional() + .default(false) + .describe( + 'If true, throw error on API failures. If false (default), return error details' + ), + }) + ), handler: async (ctx, input) => { const typedInput = input as { ip: string; failOnError?: boolean }; try { diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/zendesk/zendesk.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/zendesk/zendesk.ts index f72fd2d297a56..9d61550e756ee 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/zendesk/zendesk.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/zendesk/zendesk.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ActionContext, ConnectorSpec } from '../../connector_spec'; const buildBaseUrl = (ctx: ActionContext): string => `https://${String((ctx.config?.subdomain as string) ?? '').trim()}.zendesk.com/api/v2`; @@ -57,59 +57,63 @@ export const ZendeskConnector: ConnectorSpec = { ], }, - schema: z.object({ - subdomain: z - .string() - .min(1) - .describe('Your Zendesk subdomain') - .meta({ - widget: 'text', - label: i18n.translate('core.kibanaConnectorSpecs.zendesk.config.subdomain.label', { - defaultMessage: 'Subdomain', + schema: lazySchema(() => + z.object({ + subdomain: z + .string() + .min(1) + .describe('Your Zendesk subdomain') + .meta({ + widget: 'text', + label: i18n.translate('core.kibanaConnectorSpecs.zendesk.config.subdomain.label', { + defaultMessage: 'Subdomain', + }), + placeholder: 'your-company', + helpText: i18n.translate('core.kibanaConnectorSpecs.zendesk.config.subdomain.helpText', { + defaultMessage: + 'The subdomain for your Zendesk account (e.g. your-company for https://your-company.zendesk.com)', + }), }), - placeholder: 'your-company', - helpText: i18n.translate('core.kibanaConnectorSpecs.zendesk.config.subdomain.helpText', { - defaultMessage: - 'The subdomain for your Zendesk account (e.g. your-company for https://your-company.zendesk.com)', - }), - }), - }), + }) + ), actions: { search: { isTool: true, description: 'Search across Zendesk data (tickets, users, organizations, articles). Use when you need to find items by keyword or criteria.', - input: z.object({ - query: z - .string() - .describe( - 'Zendesk query syntax. Supports keywords, field filters (field:value), type filters (type:ticket|user|organization|group), status filters (status:open|pending|solved|closed), assignee filters (assignee:me or assignee:), tags (tags:), date filters (created>YYYY-MM-DD, updated + z.object({ + query: z + .string() + .describe( + 'Zendesk query syntax. Supports keywords, field filters (field:value), type filters (type:ticket|user|organization|group), status filters (status:open|pending|solved|closed), assignee filters (assignee:me or assignee:), tags (tags:), date filters (created>YYYY-MM-DD, updated { const baseUrl = buildBaseUrl(ctx); const params: Record = { @@ -129,20 +133,22 @@ export const ZendeskConnector: ConnectorSpec = { isTool: true, description: 'List Zendesk tickets. Use when you need to browse or filter tickets by page. For keyword or criteria-based lookups, prefer the search action instead.', - input: z.object({ - page: z.number().default(1).describe('Page number for pagination. Defaults to 1.'), - perPage: z - .number() - .max(100) - .default(25) - .describe('Number of tickets per page (max 100). Defaults to 25.'), - include: z - .string() - .optional() - .describe( - 'Comma-separated sideloads with no spaces. Valid options: users, groups, organizations. Examples: "users", "users,groups", "users,groups,organizations".' - ), - }), + input: lazySchema(() => + z.object({ + page: z.number().default(1).describe('Page number for pagination. Defaults to 1.'), + perPage: z + .number() + .max(100) + .default(25) + .describe('Number of tickets per page (max 100). Defaults to 25.'), + include: z + .string() + .optional() + .describe( + 'Comma-separated sideloads with no spaces. Valid options: users, groups, organizations. Examples: "users", "users,groups", "users,groups,organizations".' + ), + }) + ), handler: async (ctx, input) => { const baseUrl = buildBaseUrl(ctx); const params: Record = {}; @@ -158,9 +164,11 @@ export const ZendeskConnector: ConnectorSpec = { isTool: true, description: 'Get the full details of a single Zendesk ticket by ID, including metadata and comment count. Use when you already have a ticket ID and need the complete record.', - input: z.object({ - ticketId: z.string().describe('The Zendesk ticket ID (numeric, e.g. "12345").'), - }), + input: lazySchema(() => + z.object({ + ticketId: z.string().describe('The Zendesk ticket ID (numeric, e.g. "12345").'), + }) + ), handler: async (ctx, input) => { const baseUrl = buildBaseUrl(ctx); const response = await ctx.client.get(`${baseUrl}/tickets/${input.ticketId}.json`, { @@ -174,25 +182,29 @@ export const ZendeskConnector: ConnectorSpec = { isTool: true, description: 'List comments on a Zendesk ticket (the conversation thread, including both public and private comments). Use when you have a ticket ID and need to read the full discussion.', - input: z.object({ - ticketId: z.string().describe('The Zendesk ticket ID (numeric, e.g. "12345").'), - page: z.number().default(1).describe('Page number for pagination. Defaults to 1.'), - perPage: z - .number() - .max(100) - .default(25) - .describe('Number of comments per page (max 100). Defaults to 25.'), - include: z - .string() - .optional() - .describe( - 'Comma-separated list of resources to sideload (e.g. "users" to include author details).' - ), - includeInlineImages: z - .boolean() - .optional() - .describe('When true, inline images are included in comment bodies. Defaults to false.'), - }), + input: lazySchema(() => + z.object({ + ticketId: z.string().describe('The Zendesk ticket ID (numeric, e.g. "12345").'), + page: z.number().default(1).describe('Page number for pagination. Defaults to 1.'), + perPage: z + .number() + .max(100) + .default(25) + .describe('Number of comments per page (max 100). Defaults to 25.'), + include: z + .string() + .optional() + .describe( + 'Comma-separated list of resources to sideload (e.g. "users" to include author details).' + ), + includeInlineImages: z + .boolean() + .optional() + .describe( + 'When true, inline images are included in comment bodies. Defaults to false.' + ), + }) + ), handler: async (ctx, input) => { const baseUrl = buildBaseUrl(ctx); const params: Record = {}; @@ -213,7 +225,7 @@ export const ZendeskConnector: ConnectorSpec = { isTool: true, description: 'Get the currently authenticated Zendesk user. Returns the user record for the API credentials in use. Useful for verifying which account is connected or resolving your own agent/user ID.', - input: z.object({}), + input: lazySchema(() => z.object({})), handler: async (ctx) => { const baseUrl = buildBaseUrl(ctx); const response = await ctx.client.get(`${baseUrl}/users/me.json`); diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/zoom/types.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/zoom/types.ts index c11e28e51704e..72193cc4aec6a 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/zoom/types.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/zoom/types.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; // --------------------------------------------------------------------------- // Constants @@ -19,164 +19,192 @@ export const ZOOM_DEFAULT_MAX_RECORDING_CONTENT_CHARS = 100_000; // Shared output schemas — reusable shapes for Zoom API objects // --------------------------------------------------------------------------- -export const ZoomPaginationOutputSchema = z.object({ - page_size: z.number().optional().describe('Number of records per page'), - next_page_token: z.string().optional().describe('Token to fetch the next page of results'), - total_records: z.number().optional().describe('Total number of records'), -}); +export const ZoomPaginationOutputSchema = lazySchema(() => + z.object({ + page_size: z.number().optional().describe('Number of records per page'), + next_page_token: z.string().optional().describe('Token to fetch the next page of results'), + total_records: z.number().optional().describe('Total number of records'), + }) +); -export const ZoomMeetingSummarySchema = z.object({ - id: z.number().optional().describe('Numeric meeting ID'), - uuid: z.string().optional().describe('Meeting UUID'), - topic: z.string().optional().describe('Meeting topic/title'), - type: z - .number() - .optional() - .describe( - 'Meeting type: 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time' - ), - start_time: z.string().optional().describe('Scheduled start time (ISO 8601)'), - duration: z.number().optional().describe('Scheduled duration in minutes'), - timezone: z.string().optional().describe('Timezone of the meeting'), - join_url: z.string().optional().describe('URL to join the meeting'), - password: z.string().optional().describe('Meeting passcode required to join'), -}); +export const ZoomMeetingSummarySchema = lazySchema(() => + z.object({ + id: z.number().optional().describe('Numeric meeting ID'), + uuid: z.string().optional().describe('Meeting UUID'), + topic: z.string().optional().describe('Meeting topic/title'), + type: z + .number() + .optional() + .describe( + 'Meeting type: 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time' + ), + start_time: z.string().optional().describe('Scheduled start time (ISO 8601)'), + duration: z.number().optional().describe('Scheduled duration in minutes'), + timezone: z.string().optional().describe('Timezone of the meeting'), + join_url: z.string().optional().describe('URL to join the meeting'), + password: z.string().optional().describe('Meeting passcode required to join'), + }) +); -export const ZoomRecordingFileSchema = z.object({ - id: z.string().optional().describe('Recording file ID'), - recording_type: z - .string() - .optional() - .describe( - 'Type of recording: shared_screen_with_speaker_view, audio_only, audio_transcript, chat_file, timeline, summary, etc.' - ), - file_type: z.string().optional().describe('File format: MP4, M4A, VTT, TXT, JSON, etc.'), - file_size: z.number().optional().describe('File size in bytes'), - download_url: z.string().optional().describe('URL to download the recording file'), - status: z.string().optional().describe('Recording status: completed or processing'), -}); +export const ZoomRecordingFileSchema = lazySchema(() => + z.object({ + id: z.string().optional().describe('Recording file ID'), + recording_type: z + .string() + .optional() + .describe( + 'Type of recording: shared_screen_with_speaker_view, audio_only, audio_transcript, chat_file, timeline, summary, etc.' + ), + file_type: z.string().optional().describe('File format: MP4, M4A, VTT, TXT, JSON, etc.'), + file_size: z.number().optional().describe('File size in bytes'), + download_url: z.string().optional().describe('URL to download the recording file'), + status: z.string().optional().describe('Recording status: completed or processing'), + }) +); -export const ZoomParticipantSchema = z.object({ - name: z.string().optional().describe('Participant display name'), - user_email: z.string().optional().describe('Participant email address'), - join_time: z.string().optional().describe('Time the participant joined (ISO 8601)'), - leave_time: z.string().optional().describe('Time the participant left (ISO 8601)'), - duration: z.number().optional().describe('Time in meeting in seconds'), -}); +export const ZoomParticipantSchema = lazySchema(() => + z.object({ + name: z.string().optional().describe('Participant display name'), + user_email: z.string().optional().describe('Participant email address'), + join_time: z.string().optional().describe('Time the participant joined (ISO 8601)'), + leave_time: z.string().optional().describe('Time the participant left (ISO 8601)'), + duration: z.number().optional().describe('Time in meeting in seconds'), + }) +); -export const ZoomRegistrantSchema = z.object({ - email: z.string().optional().describe('Registrant email address'), - first_name: z.string().optional().describe('Registrant first name'), - last_name: z.string().optional().describe('Registrant last name'), - status: z.string().optional().describe('Registration status: approved, pending, or denied'), -}); +export const ZoomRegistrantSchema = lazySchema(() => + z.object({ + email: z.string().optional().describe('Registrant email address'), + first_name: z.string().optional().describe('Registrant first name'), + last_name: z.string().optional().describe('Registrant last name'), + status: z.string().optional().describe('Registration status: approved, pending, or denied'), + }) +); -export const ZoomUserProfileSchema = z.object({ - id: z.string().optional().describe('Zoom user ID'), - display_name: z.string().optional().describe('Display name'), - first_name: z.string().optional().describe('First name'), - last_name: z.string().optional().describe('Last name'), - email: z.string().optional().describe('Email address'), - type: z.number().optional().describe('Account type: 1=Basic, 2=Licensed, 3=On-Prem, 99=None'), - role_name: z.string().optional().describe('Role name (Owner, Admin, Member)'), - status: z.string().optional().describe('Account status: active, inactive, or pending'), - timezone: z.string().optional().describe('User timezone'), - language: z.string().optional().describe('Preferred language'), - pmi: z.number().optional().describe('Personal Meeting ID'), - personal_meeting_url: z.string().optional().describe('Personal meeting room URL'), - dept: z.string().optional().describe('Department'), - job_title: z.string().optional().describe('Job title'), - company: z.string().optional().describe('Company'), - location: z.string().optional().describe('Location'), - account_id: z.string().optional().describe('Zoom account ID'), - created_at: z.string().optional().describe('Account creation timestamp (ISO 8601)'), - last_login_time: z.string().optional().describe('Last login timestamp (ISO 8601)'), -}); +export const ZoomUserProfileSchema = lazySchema(() => + z.object({ + id: z.string().optional().describe('Zoom user ID'), + display_name: z.string().optional().describe('Display name'), + first_name: z.string().optional().describe('First name'), + last_name: z.string().optional().describe('Last name'), + email: z.string().optional().describe('Email address'), + type: z.number().optional().describe('Account type: 1=Basic, 2=Licensed, 3=On-Prem, 99=None'), + role_name: z.string().optional().describe('Role name (Owner, Admin, Member)'), + status: z.string().optional().describe('Account status: active, inactive, or pending'), + timezone: z.string().optional().describe('User timezone'), + language: z.string().optional().describe('Preferred language'), + pmi: z.number().optional().describe('Personal Meeting ID'), + personal_meeting_url: z.string().optional().describe('Personal meeting room URL'), + dept: z.string().optional().describe('Department'), + job_title: z.string().optional().describe('Job title'), + company: z.string().optional().describe('Company'), + location: z.string().optional().describe('Location'), + account_id: z.string().optional().describe('Zoom account ID'), + created_at: z.string().optional().describe('Account creation timestamp (ISO 8601)'), + last_login_time: z.string().optional().describe('Last login timestamp (ISO 8601)'), + }) +); // --------------------------------------------------------------------------- // Input schemas // --------------------------------------------------------------------------- -export const ZoomWhoAmIInputSchema = z.object({}); +export const ZoomWhoAmIInputSchema = lazySchema(() => z.object({})); export type ZoomWhoAmIInput = z.infer; -export const ZoomListMeetingsInputSchema = z.object({ - userId: z - .string() - .default('me') - .describe('User ID or email. Use "me" for the authenticated user.'), - type: z - .enum(['scheduled', 'live', 'upcoming', 'upcoming_meetings', 'previous_meetings']) - .default('upcoming') - .describe( - 'Meeting type filter. Values: scheduled (all scheduled meetings), live (in-progress), upcoming (default, future meetings), upcoming_meetings (similar to upcoming), previous_meetings (past meetings).' - ), - pageSize: z.number().min(1).max(300).optional().describe('Number of results per page (1-300)'), - nextPageToken: z.string().optional().describe('Pagination token from a previous response'), -}); +export const ZoomListMeetingsInputSchema = lazySchema(() => + z.object({ + userId: z + .string() + .default('me') + .describe('User ID or email. Use "me" for the authenticated user.'), + type: z + .enum(['scheduled', 'live', 'upcoming', 'upcoming_meetings', 'previous_meetings']) + .default('upcoming') + .describe( + 'Meeting type filter. Values: scheduled (all scheduled meetings), live (in-progress), upcoming (default, future meetings), upcoming_meetings (similar to upcoming), previous_meetings (past meetings).' + ), + pageSize: z.number().min(1).max(300).optional().describe('Number of results per page (1-300)'), + nextPageToken: z.string().optional().describe('Pagination token from a previous response'), + }) +); export type ZoomListMeetingsInput = z.infer; -export const ZoomGetMeetingDetailsInputSchema = z.object({ - meetingId: z.string().describe('Meeting ID or UUID'), -}); +export const ZoomGetMeetingDetailsInputSchema = lazySchema(() => + z.object({ + meetingId: z.string().describe('Meeting ID or UUID'), + }) +); export type ZoomGetMeetingDetailsInput = z.infer; -export const ZoomGetPastMeetingDetailsInputSchema = z.object({ - meetingId: z.string().describe('Past meeting ID or UUID'), -}); +export const ZoomGetPastMeetingDetailsInputSchema = lazySchema(() => + z.object({ + meetingId: z.string().describe('Past meeting ID or UUID'), + }) +); export type ZoomGetPastMeetingDetailsInput = z.infer; -export const ZoomGetMeetingRecordingsInputSchema = z.object({ - meetingId: z.string().describe('Meeting ID or UUID'), -}); +export const ZoomGetMeetingRecordingsInputSchema = lazySchema(() => + z.object({ + meetingId: z.string().describe('Meeting ID or UUID'), + }) +); export type ZoomGetMeetingRecordingsInput = z.infer; -export const ZoomListUserRecordingsInputSchema = z.object({ - userId: z - .string() - .default('me') - .describe('User ID or email. Use "me" for the authenticated user.'), - from: z.string().optional().describe('Start date (YYYY-MM-DD). Defaults to current date.'), - to: z.string().optional().describe('End date (YYYY-MM-DD). Range cannot exceed 1 month.'), - pageSize: z.number().min(1).max(300).optional().describe('Number of results per page (1-300)'), - nextPageToken: z.string().optional().describe('Pagination token from a previous response'), -}); +export const ZoomListUserRecordingsInputSchema = lazySchema(() => + z.object({ + userId: z + .string() + .default('me') + .describe('User ID or email. Use "me" for the authenticated user.'), + from: z.string().optional().describe('Start date (YYYY-MM-DD). Defaults to current date.'), + to: z.string().optional().describe('End date (YYYY-MM-DD). Range cannot exceed 1 month.'), + pageSize: z.number().min(1).max(300).optional().describe('Number of results per page (1-300)'), + nextPageToken: z.string().optional().describe('Pagination token from a previous response'), + }) +); export type ZoomListUserRecordingsInput = z.infer; -export const ZoomDownloadRecordingFileInputSchema = z.object({ - downloadUrl: z - .string() - .url() - .describe( - 'The download_url from a recording file object (obtained via getMeetingRecordings or listUserRecordings)' - ), - maxChars: z - .number() - .int() - .min(1) - .optional() - .describe( - `Maximum characters to return. Defaults to ${ZOOM_DEFAULT_MAX_RECORDING_CONTENT_CHARS}. Content exceeding this limit is truncated.` - ), -}); +export const ZoomDownloadRecordingFileInputSchema = lazySchema(() => + z.object({ + downloadUrl: z + .string() + .url() + .describe( + 'The download_url from a recording file object (obtained via getMeetingRecordings or listUserRecordings)' + ), + maxChars: z + .number() + .int() + .min(1) + .optional() + .describe( + `Maximum characters to return. Defaults to ${ZOOM_DEFAULT_MAX_RECORDING_CONTENT_CHARS}. Content exceeding this limit is truncated.` + ), + }) +); export type ZoomDownloadRecordingFileInput = z.infer; -export const ZoomGetMeetingParticipantsInputSchema = z.object({ - meetingId: z.string().describe('Past meeting ID or UUID'), - pageSize: z.number().min(1).max(300).optional().describe('Number of results per page (1-300)'), - nextPageToken: z.string().optional().describe('Pagination token from a previous response'), -}); +export const ZoomGetMeetingParticipantsInputSchema = lazySchema(() => + z.object({ + meetingId: z.string().describe('Past meeting ID or UUID'), + pageSize: z.number().min(1).max(300).optional().describe('Number of results per page (1-300)'), + nextPageToken: z.string().optional().describe('Pagination token from a previous response'), + }) +); export type ZoomGetMeetingParticipantsInput = z.infer; -export const ZoomGetMeetingRegistrantsInputSchema = z.object({ - meetingId: z.string().describe('Meeting ID or UUID'), - status: z - .enum(['pending', 'approved', 'denied']) - .optional() - .describe('Filter by registration status. Defaults to approved.'), - pageSize: z.number().min(1).max(300).optional().describe('Number of results per page (1-300)'), - nextPageToken: z.string().optional().describe('Pagination token from a previous response'), -}); +export const ZoomGetMeetingRegistrantsInputSchema = lazySchema(() => + z.object({ + meetingId: z.string().describe('Meeting ID or UUID'), + status: z + .enum(['pending', 'approved', 'denied']) + .optional() + .describe('Filter by registration status. Defaults to approved.'), + pageSize: z.number().min(1).max(300).optional().describe('Number of results per page (1-300)'), + nextPageToken: z.string().optional().describe('Pagination token from a previous response'), + }) +); export type ZoomGetMeetingRegistrantsInput = z.infer; // --------------------------------------------------------------------------- diff --git a/src/platform/packages/shared/kbn-connector-specs/src/specs/zoom/zoom.ts b/src/platform/packages/shared/kbn-connector-specs/src/specs/zoom/zoom.ts index 585fb0c10ddb1..2870d776483e8 100644 --- a/src/platform/packages/shared/kbn-connector-specs/src/specs/zoom/zoom.ts +++ b/src/platform/packages/shared/kbn-connector-specs/src/specs/zoom/zoom.ts @@ -37,7 +37,7 @@ */ import { i18n } from '@kbn/i18n'; -import { z } from '@kbn/zod/v4'; +import { z, lazySchema } from '@kbn/zod/v4'; import type { ConnectorSpec } from '../../connector_spec'; import type { AnyRecord, @@ -158,9 +158,11 @@ export const Zoom: ConnectorSpec = { description: 'List meetings for a user. Use type=upcoming (default) for future meetings, type=live for in-progress meetings, type=scheduled for all scheduled meetings, or type=previous_meetings for past meetings. Returns meeting topics, start times, durations, and join URLs.', input: ZoomListMeetingsInputSchema, - output: ZoomPaginationOutputSchema.extend({ - meetings: z.array(ZoomMeetingSummarySchema).describe('Array of meeting summaries'), - }), + output: lazySchema(() => + ZoomPaginationOutputSchema.extend({ + meetings: z.array(ZoomMeetingSummarySchema).describe('Array of meeting summaries'), + }) + ), handler: async (ctx, input) => { const typedInput: ZoomListMeetingsInput = ZoomListMeetingsInputSchema.parse(input); ctx.log.debug( @@ -192,25 +194,27 @@ export const Zoom: ConnectorSpec = { description: 'Get details of a scheduled or recurring meeting, including topic, agenda, start time, duration, timezone, host info, join URL, and settings. Use this for upcoming/recurring meetings to understand what a meeting is about before looking at recordings or participants.', input: ZoomGetMeetingDetailsInputSchema, - output: z.object({ - uuid: z.string().optional(), - id: z.number().optional(), - host_email: z.string().optional(), - topic: z.string().optional(), - type: z - .number() - .optional() - .describe( - 'Meeting type: 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time' - ), - status: z.string().optional().describe('Meeting status: waiting or started'), - start_time: z.string().optional(), - duration: z.number().optional().describe('Scheduled duration in minutes'), - timezone: z.string().optional(), - agenda: z.string().optional().describe('Meeting agenda/description'), - join_url: z.string().optional(), - password: z.string().optional().describe('Meeting passcode required to join'), - }), + output: lazySchema(() => + z.object({ + uuid: z.string().optional(), + id: z.number().optional(), + host_email: z.string().optional(), + topic: z.string().optional(), + type: z + .number() + .optional() + .describe( + 'Meeting type: 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time' + ), + status: z.string().optional().describe('Meeting status: waiting or started'), + start_time: z.string().optional(), + duration: z.number().optional().describe('Scheduled duration in minutes'), + timezone: z.string().optional(), + agenda: z.string().optional().describe('Meeting agenda/description'), + join_url: z.string().optional(), + password: z.string().optional().describe('Meeting passcode required to join'), + }) + ), handler: async (ctx, input) => { const typedInput: ZoomGetMeetingDetailsInput = ZoomGetMeetingDetailsInputSchema.parse(input); @@ -240,16 +244,18 @@ export const Zoom: ConnectorSpec = { description: 'Get summary information for a meeting that has already ended. Returns total minutes, participant count, start/end times. Only works for past meetings — use getMeetingDetails for upcoming/scheduled meetings.', input: ZoomGetPastMeetingDetailsInputSchema, - output: z.object({ - uuid: z.string().optional(), - id: z.number().optional(), - topic: z.string().optional(), - start_time: z.string().optional(), - end_time: z.string().optional(), - duration: z.number().optional().describe('Actual meeting duration in minutes'), - total_minutes: z.number().optional().describe('Sum of all participant minutes'), - participants_count: z.number().optional(), - }), + output: lazySchema(() => + z.object({ + uuid: z.string().optional(), + id: z.number().optional(), + topic: z.string().optional(), + start_time: z.string().optional(), + end_time: z.string().optional(), + duration: z.number().optional().describe('Actual meeting duration in minutes'), + total_minutes: z.number().optional().describe('Sum of all participant minutes'), + participants_count: z.number().optional(), + }) + ), handler: async (ctx, input) => { const typedInput: ZoomGetPastMeetingDetailsInput = ZoomGetPastMeetingDetailsInputSchema.parse(input); @@ -275,14 +281,16 @@ export const Zoom: ConnectorSpec = { description: 'Get cloud recording files for a specific meeting. Returns recording_files entries with recording_type (e.g. audio_transcript for VTT transcripts, chat_file for TXT chat logs, shared_screen_with_speaker_view for MP4 video), file_type, file_size, download_url, and status. Use the download_url values with downloadRecordingFile to fetch the actual content.', input: ZoomGetMeetingRecordingsInputSchema, - output: z.object({ - topic: z.string().optional(), - start_time: z.string().optional(), - duration: z.number().optional(), - recording_count: z.number().optional(), - password: z.string().optional().describe('Passcode to access the recording files'), - recording_files: z.array(ZoomRecordingFileSchema), - }), + output: lazySchema(() => + z.object({ + topic: z.string().optional(), + start_time: z.string().optional(), + duration: z.number().optional(), + recording_count: z.number().optional(), + password: z.string().optional().describe('Passcode to access the recording files'), + recording_files: z.array(ZoomRecordingFileSchema), + }) + ), handler: async (ctx, input) => { const typedInput: ZoomGetMeetingRecordingsInput = ZoomGetMeetingRecordingsInputSchema.parse(input); @@ -306,16 +314,18 @@ export const Zoom: ConnectorSpec = { description: 'List cloud recordings for a user within a date range (max 1 month). Returns meetings with their recording_files entries (including audio_transcript for VTT transcripts and chat_file for TXT chat logs). Use the download_url values from recording_files with downloadRecordingFile to fetch the actual content. Use this when you need recordings across multiple meetings; use getMeetingRecordings when you already know the specific meeting ID.', input: ZoomListUserRecordingsInputSchema, - output: ZoomPaginationOutputSchema.extend({ - from: z.string().optional(), - to: z.string().optional(), - meetings: z.array( - ZoomMeetingSummarySchema.extend({ - recording_count: z.number().optional(), - recording_files: z.array(ZoomRecordingFileSchema), - }) - ), - }), + output: lazySchema(() => + ZoomPaginationOutputSchema.extend({ + from: z.string().optional(), + to: z.string().optional(), + meetings: z.array( + ZoomMeetingSummarySchema.extend({ + recording_count: z.number().optional(), + recording_files: z.array(ZoomRecordingFileSchema), + }) + ), + }) + ), handler: async (ctx, input) => { const typedInput: ZoomListUserRecordingsInput = ZoomListUserRecordingsInputSchema.parse(input); @@ -352,11 +362,13 @@ export const Zoom: ConnectorSpec = { description: 'Download a Zoom recording file by its download_url. Works for transcripts (recording_type=audio_transcript, VTT format), chat logs (recording_type=chat_file, TXT format), and other recording file types. Obtain the download_url from getMeetingRecordings or listUserRecordings — look for recording_files entries with the desired recording_type. Returns the file content as UTF-8 text, truncated to maxChars if needed. Check the truncated flag in the response.', input: ZoomDownloadRecordingFileInputSchema, - output: z.object({ - contentType: z.string().optional().describe('Content-Type header from the response'), - text: z.string().describe('File content as UTF-8 text (may be truncated)'), - truncated: z.boolean().describe('Whether the content was truncated to maxChars'), - }), + output: lazySchema(() => + z.object({ + contentType: z.string().optional().describe('Content-Type header from the response'), + text: z.string().describe('File content as UTF-8 text (may be truncated)'), + truncated: z.boolean().describe('Whether the content was truncated to maxChars'), + }) + ), handler: async (ctx, input) => { const typedInput: ZoomDownloadRecordingFileInput = ZoomDownloadRecordingFileInputSchema.parse(input); @@ -382,9 +394,11 @@ export const Zoom: ConnectorSpec = { description: 'List participants of a past meeting. Returns participant name, email, join/leave times, and duration. Only works for meetings that have ended.', input: ZoomGetMeetingParticipantsInputSchema, - output: ZoomPaginationOutputSchema.extend({ - participants: z.array(ZoomParticipantSchema), - }), + output: lazySchema(() => + ZoomPaginationOutputSchema.extend({ + participants: z.array(ZoomParticipantSchema), + }) + ), handler: async (ctx, input) => { const typedInput: ZoomGetMeetingParticipantsInput = ZoomGetMeetingParticipantsInputSchema.parse(input); @@ -414,9 +428,11 @@ export const Zoom: ConnectorSpec = { description: 'List registrants of a meeting. Works for future and past meetings that have registration enabled. Returns registrant name, email, and registration status.', input: ZoomGetMeetingRegistrantsInputSchema, - output: ZoomPaginationOutputSchema.extend({ - registrants: z.array(ZoomRegistrantSchema), - }), + output: lazySchema(() => + ZoomPaginationOutputSchema.extend({ + registrants: z.array(ZoomRegistrantSchema), + }) + ), handler: async (ctx, input) => { const typedInput: ZoomGetMeetingRegistrantsInput = ZoomGetMeetingRegistrantsInputSchema.parse(input);