diff --git a/apps/sim/app/api/tools/cloudwatch/mute-alarm/route.ts b/apps/sim/app/api/tools/cloudwatch/mute-alarm/route.ts new file mode 100644 index 00000000000..b7d435a6e46 --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/mute-alarm/route.ts @@ -0,0 +1,62 @@ +import { CloudWatchClient, DisableAlarmActionsCommand } from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudwatchMuteAlarmContract } from '@/lib/api/contracts/tools/aws/cloudwatch-mute-alarm' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('CloudWatchMuteAlarm') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudwatchMuteAlarmContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`Muting ${validatedData.alarmNames.length} CloudWatch alarm(s)`) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const command = new DisableAlarmActionsCommand({ + AlarmNames: validatedData.alarmNames, + }) + + await client.send(command) + + logger.info(`Successfully muted ${validatedData.alarmNames.length} alarm(s)`) + + return NextResponse.json({ + success: true, + output: { + success: true, + alarmNames: validatedData.alarmNames, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('MuteAlarm failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to mute CloudWatch alarm: ${toError(error).message}` }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/cloudwatch/unmute-alarm/route.ts b/apps/sim/app/api/tools/cloudwatch/unmute-alarm/route.ts new file mode 100644 index 00000000000..79357ae38f1 --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/unmute-alarm/route.ts @@ -0,0 +1,62 @@ +import { CloudWatchClient, EnableAlarmActionsCommand } from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudwatchUnmuteAlarmContract } from '@/lib/api/contracts/tools/aws/cloudwatch-unmute-alarm' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('CloudWatchUnmuteAlarm') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudwatchUnmuteAlarmContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`Unmuting ${validatedData.alarmNames.length} CloudWatch alarm(s)`) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const command = new EnableAlarmActionsCommand({ + AlarmNames: validatedData.alarmNames, + }) + + await client.send(command) + + logger.info(`Successfully unmuted ${validatedData.alarmNames.length} alarm(s)`) + + return NextResponse.json({ + success: true, + output: { + success: true, + alarmNames: validatedData.alarmNames, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('UnmuteAlarm failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to unmute CloudWatch alarm: ${toError(error).message}` }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts index 30e5245d141..585edb6a074 100644 --- a/apps/sim/blocks/blocks/cloudwatch.ts +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -8,8 +8,10 @@ import type { CloudWatchGetLogEventsResponse, CloudWatchGetMetricStatisticsResponse, CloudWatchListMetricsResponse, + CloudWatchMuteAlarmResponse, CloudWatchPutMetricDataResponse, CloudWatchQueryLogsResponse, + CloudWatchUnmuteAlarmResponse, } from '@/tools/cloudwatch/types' export const CloudWatchBlock: BlockConfig< @@ -21,6 +23,8 @@ export const CloudWatchBlock: BlockConfig< | CloudWatchListMetricsResponse | CloudWatchGetMetricStatisticsResponse | CloudWatchPutMetricDataResponse + | CloudWatchMuteAlarmResponse + | CloudWatchUnmuteAlarmResponse > = { type: 'cloudwatch', name: 'CloudWatch', @@ -47,6 +51,8 @@ export const CloudWatchBlock: BlockConfig< { label: 'Get Metric Statistics', id: 'get_metric_statistics' }, { label: 'Publish Metric', id: 'put_metric_data' }, { label: 'Describe Alarms', id: 'describe_alarms' }, + { label: 'Mute Alarm', id: 'mute_alarm' }, + { label: 'Unmute Alarm', id: 'unmute_alarm' }, ], value: () => 'query_logs', }, @@ -360,6 +366,14 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, value: () => '', condition: { field: 'operation', value: 'describe_alarms' }, }, + { + id: 'alarmNames', + title: 'Alarm Names', + type: 'short-input', + placeholder: 'my-alarm-1, my-alarm-2', + condition: { field: 'operation', value: ['mute_alarm', 'unmute_alarm'] }, + required: { field: 'operation', value: ['mute_alarm', 'unmute_alarm'] }, + }, { id: 'limit', title: 'Limit', @@ -389,6 +403,8 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, 'cloudwatch_get_metric_statistics', 'cloudwatch_put_metric_data', 'cloudwatch_describe_alarms', + 'cloudwatch_mute_alarm', + 'cloudwatch_unmute_alarm', ], config: { tool: (params) => { @@ -409,6 +425,10 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, return 'cloudwatch_put_metric_data' case 'describe_alarms': return 'cloudwatch_describe_alarms' + case 'mute_alarm': + return 'cloudwatch_mute_alarm' + case 'unmute_alarm': + return 'cloudwatch_unmute_alarm' default: throw new Error(`Invalid CloudWatch operation: ${params.operation}`) } @@ -613,6 +633,33 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, ...(parsedLimit !== undefined && { limit: parsedLimit }), } + case 'mute_alarm': + case 'unmute_alarm': { + const alarmNames = rest.alarmNames + if (!alarmNames) { + throw new Error('Alarm names are required') + } + + const names = + typeof alarmNames === 'string' + ? alarmNames + .split(',') + .map((n: string) => n.trim()) + .filter(Boolean) + : alarmNames + + if (!Array.isArray(names) || names.length === 0) { + throw new Error('At least one alarm name is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + alarmNames: names, + } + } + default: throw new Error(`Invalid CloudWatch operation: ${operation}`) } @@ -653,6 +700,7 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, description: 'Alarm state filter (OK, ALARM, INSUFFICIENT_DATA)', }, alarmType: { type: 'string', description: 'Alarm type filter (MetricAlarm, CompositeAlarm)' }, + alarmNames: { type: 'string', description: 'Comma-separated alarm names to mute or unmute' }, limit: { type: 'number', description: 'Maximum number of results' }, }, outputs: { @@ -696,9 +744,13 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, type: 'array', description: 'CloudWatch alarms with state and configuration', }, + alarmNames: { + type: 'array', + description: 'Names of the alarms that were muted or unmuted', + }, success: { type: 'boolean', - description: 'Whether the published metric was successful', + description: 'Whether the operation completed successfully', }, namespace: { type: 'string', diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-mute-alarm.ts b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-mute-alarm.ts new file mode 100644 index 00000000000..cb95e59d9c3 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-mute-alarm.ts @@ -0,0 +1,43 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const MuteAlarmSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + alarmNames: z + .array(z.string().min(1, 'Alarm name cannot be empty')) + .min(1, 'At least one alarm name is required') + .max(100, 'At most 100 alarm names are allowed per request'), +}) + +const MuteAlarmResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + success: z.literal(true), + alarmNames: z.array(z.string()), + }), +}) + +export const awsCloudwatchMuteAlarmContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudwatch/mute-alarm', + body: MuteAlarmSchema, + response: { mode: 'json', schema: MuteAlarmResponseSchema }, +}) +export type AwsCloudwatchMuteAlarmRequest = ContractBodyInput +export type AwsCloudwatchMuteAlarmBody = ContractBody +export type AwsCloudwatchMuteAlarmResponse = ContractJsonResponse< + typeof awsCloudwatchMuteAlarmContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-unmute-alarm.ts b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-unmute-alarm.ts new file mode 100644 index 00000000000..f0fd1427a83 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-unmute-alarm.ts @@ -0,0 +1,45 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const UnmuteAlarmSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + alarmNames: z + .array(z.string().min(1, 'Alarm name cannot be empty')) + .min(1, 'At least one alarm name is required') + .max(100, 'At most 100 alarm names are allowed per request'), +}) + +const UnmuteAlarmResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + success: z.literal(true), + alarmNames: z.array(z.string()), + }), +}) + +export const awsCloudwatchUnmuteAlarmContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudwatch/unmute-alarm', + body: UnmuteAlarmSchema, + response: { mode: 'json', schema: UnmuteAlarmResponseSchema }, +}) +export type AwsCloudwatchUnmuteAlarmRequest = ContractBodyInput< + typeof awsCloudwatchUnmuteAlarmContract +> +export type AwsCloudwatchUnmuteAlarmBody = ContractBody +export type AwsCloudwatchUnmuteAlarmResponse = ContractJsonResponse< + typeof awsCloudwatchUnmuteAlarmContract +> diff --git a/apps/sim/tools/cloudwatch/index.ts b/apps/sim/tools/cloudwatch/index.ts index ec67cd532f6..0d92b5662fe 100644 --- a/apps/sim/tools/cloudwatch/index.ts +++ b/apps/sim/tools/cloudwatch/index.ts @@ -4,8 +4,10 @@ import { describeLogStreamsTool } from '@/tools/cloudwatch/describe_log_streams' import { getLogEventsTool } from '@/tools/cloudwatch/get_log_events' import { getMetricStatisticsTool } from '@/tools/cloudwatch/get_metric_statistics' import { listMetricsTool } from '@/tools/cloudwatch/list_metrics' +import { muteAlarmTool } from '@/tools/cloudwatch/mute_alarm' import { putMetricDataTool } from '@/tools/cloudwatch/put_metric_data' import { queryLogsTool } from '@/tools/cloudwatch/query_logs' +import { unmuteAlarmTool } from '@/tools/cloudwatch/unmute_alarm' export * from './types' @@ -15,5 +17,7 @@ export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool export const cloudwatchGetLogEventsTool = getLogEventsTool export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool export const cloudwatchListMetricsTool = listMetricsTool +export const cloudwatchMuteAlarmTool = muteAlarmTool export const cloudwatchPutMetricDataTool = putMetricDataTool export const cloudwatchQueryLogsTool = queryLogsTool +export const cloudwatchUnmuteAlarmTool = unmuteAlarmTool diff --git a/apps/sim/tools/cloudwatch/mute_alarm.ts b/apps/sim/tools/cloudwatch/mute_alarm.ts new file mode 100644 index 00000000000..37591920dbe --- /dev/null +++ b/apps/sim/tools/cloudwatch/mute_alarm.ts @@ -0,0 +1,75 @@ +import type { + CloudWatchMuteAlarmParams, + CloudWatchMuteAlarmResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const muteAlarmTool: ToolConfig = { + id: 'cloudwatch_mute_alarm', + name: 'CloudWatch Mute Alarm', + description: 'Disable notification actions on one or more CloudWatch alarms', + version: '1.0.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + alarmNames: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Names of the CloudWatch alarms to mute', + }, + }, + + request: { + url: '/api/tools/cloudwatch/mute-alarm', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + alarmNames: params.alarmNames, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to mute CloudWatch alarm') + } + + return { + success: true, + output: data.output, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the alarms were muted successfully' }, + alarmNames: { + type: 'array', + description: 'Names of the alarms that were muted', + items: { type: 'string' }, + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/types.ts b/apps/sim/tools/cloudwatch/types.ts index f4172283029..0e243ab12ca 100644 --- a/apps/sim/tools/cloudwatch/types.ts +++ b/apps/sim/tools/cloudwatch/types.ts @@ -163,3 +163,25 @@ export interface CloudWatchPutMetricDataResponse extends ToolResponse { timestamp: string } } + +export interface CloudWatchMuteAlarmParams extends CloudWatchConnectionConfig { + alarmNames: string[] +} + +export interface CloudWatchMuteAlarmResponse extends ToolResponse { + output: { + success: boolean + alarmNames: string[] + } +} + +export interface CloudWatchUnmuteAlarmParams extends CloudWatchConnectionConfig { + alarmNames: string[] +} + +export interface CloudWatchUnmuteAlarmResponse extends ToolResponse { + output: { + success: boolean + alarmNames: string[] + } +} diff --git a/apps/sim/tools/cloudwatch/unmute_alarm.ts b/apps/sim/tools/cloudwatch/unmute_alarm.ts new file mode 100644 index 00000000000..a6e0a4270ed --- /dev/null +++ b/apps/sim/tools/cloudwatch/unmute_alarm.ts @@ -0,0 +1,78 @@ +import type { + CloudWatchUnmuteAlarmParams, + CloudWatchUnmuteAlarmResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const unmuteAlarmTool: ToolConfig< + CloudWatchUnmuteAlarmParams, + CloudWatchUnmuteAlarmResponse +> = { + id: 'cloudwatch_unmute_alarm', + name: 'CloudWatch Unmute Alarm', + description: 'Re-enable notification actions on one or more CloudWatch alarms', + version: '1.0.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + alarmNames: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Names of the CloudWatch alarms to unmute', + }, + }, + + request: { + url: '/api/tools/cloudwatch/unmute-alarm', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + alarmNames: params.alarmNames, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to unmute CloudWatch alarm') + } + + return { + success: true, + output: data.output, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the alarms were unmuted successfully' }, + alarmNames: { + type: 'array', + description: 'Names of the alarms that were unmuted', + items: { type: 'string' }, + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 014ca723df2..c71385aa0d9 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -350,8 +350,10 @@ import { cloudwatchGetLogEventsTool, cloudwatchGetMetricStatisticsTool, cloudwatchListMetricsTool, + cloudwatchMuteAlarmTool, cloudwatchPutMetricDataTool, cloudwatchQueryLogsTool, + cloudwatchUnmuteAlarmTool, } from '@/tools/cloudwatch' import { confluenceAddLabelTool, @@ -3813,8 +3815,10 @@ export const tools: Record = { cloudwatch_get_log_events: cloudwatchGetLogEventsTool, cloudwatch_get_metric_statistics: cloudwatchGetMetricStatisticsTool, cloudwatch_list_metrics: cloudwatchListMetricsTool, + cloudwatch_mute_alarm: cloudwatchMuteAlarmTool, cloudwatch_put_metric_data: cloudwatchPutMetricDataTool, cloudwatch_query_logs: cloudwatchQueryLogsTool, + cloudwatch_unmute_alarm: cloudwatchUnmuteAlarmTool, crowdstrike_get_sensor_aggregates: crowdstrikeGetSensorAggregatesTool, crowdstrike_get_sensor_details: crowdstrikeGetSensorDetailsTool, crowdstrike_query_sensors: crowdstrikeQuerySensorsTool, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 1081576504d..6b4425e7cd7 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 736, - zodRoutes: 736, + totalRoutes: 738, + zodRoutes: 738, nonZodRoutes: 0, } as const