AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > exec-aide — Gmail inbox monitor with AI classification and Slack alerts Globals: Function: Runtime: python3.12 Architectures: - arm64 MemorySize: 256 Timeout: 120 Environment: Variables: TABLE_NAME: !Ref ExecAideTable SECRET_GMAIL: exec-aide/gmail-oauth SECRET_SLACK: exec-aide/slack-credentials SSM_PREFIX: /exec-aide Resources: # ── DynamoDB ────────────────────────────────────────────── ExecAideTable: Type: AWS::DynamoDB::Table Properties: TableName: exec-aide BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: pk AttributeType: S - AttributeName: sk AttributeType: S - AttributeName: classified_date AttributeType: S KeySchema: - AttributeName: pk KeyType: HASH - AttributeName: sk KeyType: RANGE GlobalSecondaryIndexes: - IndexName: by-date KeySchema: - AttributeName: classified_date KeyType: HASH - AttributeName: sk KeyType: RANGE Projection: ProjectionType: ALL TimeToLiveSpecification: AttributeName: ttl Enabled: true # ── CloudWatch Log Groups ──────────────────────────────── FetchClassifyLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/exec-aide-fetch-classify RetentionInDays: 60 DailyDigestLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/exec-aide-daily-digest RetentionInDays: 60 # ── Lambda Functions ───────────────────────────────────── FetchClassifyFunction: Type: AWS::Serverless::Function DependsOn: FetchClassifyLogGroup Properties: FunctionName: exec-aide-fetch-classify Handler: fetch_classify.app.lambda_handler CodeUri: src/ Events: PollSchedule: Type: Schedule Properties: Schedule: rate(15 minutes) Description: Poll Gmail for new messages Enabled: true Policies: - DynamoDBCrudPolicy: TableName: !Ref ExecAideTable - Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:exec-aide/gmail-oauth-* - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:exec-aide/slack-credentials-* - Effect: Allow Action: - secretsmanager:PutSecretValue Resource: - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:exec-aide/gmail-oauth-* - Effect: Allow Action: - ssm:GetParametersByPath - ssm:GetParameter Resource: - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/exec-aide/* - Effect: Allow Action: - bedrock:InvokeModel Resource: - arn:aws:bedrock:*::foundation-model/anthropic.* - !Sub arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:inference-profile/us.anthropic.* DailyDigestFunction: Type: AWS::Serverless::Function DependsOn: DailyDigestLogGroup Properties: FunctionName: exec-aide-daily-digest Handler: daily_digest.app.lambda_handler CodeUri: src/ Policies: - DynamoDBCrudPolicy: TableName: !Ref ExecAideTable - Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:exec-aide/gmail-oauth-* - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:exec-aide/slack-credentials-* - Effect: Allow Action: - secretsmanager:PutSecretValue Resource: - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:exec-aide/gmail-oauth-* - Effect: Allow Action: - ssm:GetParametersByPath - ssm:GetParameter Resource: - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/exec-aide/* # ── ECS / Fargate (Socket Mode listener) ────────────────── ListenerLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /ecs/exec-aide-listener RetentionInDays: 60 EcsCluster: Type: AWS::ECS::Cluster Properties: ClusterName: exec-aide ListenerTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Family: exec-aide-listener Cpu: "256" Memory: "512" NetworkMode: awsvpc RequiresCompatibilities: - FARGATE RuntimePlatform: CpuArchitecture: ARM64 OperatingSystemFamily: LINUX ExecutionRoleArn: !GetAtt ListenerExecutionRole.Arn TaskRoleArn: !GetAtt ListenerTaskRole.Arn ContainerDefinitions: - Name: listener Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/exec-aide-listener:latest Essential: true Environment: - Name: TABLE_NAME Value: !Ref ExecAideTable - Name: SECRET_SLACK Value: exec-aide/slack-credentials - Name: SSM_PREFIX Value: /exec-aide LogConfiguration: LogDriver: awslogs Options: awslogs-group: /ecs/exec-aide-listener awslogs-region: !Ref AWS::Region awslogs-stream-prefix: listener ListenerService: Type: AWS::ECS::Service Properties: ServiceName: exec-aide-listener Cluster: !Ref EcsCluster TaskDefinition: !Ref ListenerTaskDefinition DesiredCount: 1 LaunchType: FARGATE NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED Subnets: - !Ref ListenerSubnet SecurityGroups: - !Ref ListenerSecurityGroup ListenerSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref ListenerVpc CidrBlock: 10.30.0.0/24 MapPublicIpOnLaunch: true AvailabilityZone: !Select [0, !GetAZs ""] Tags: - Key: Name Value: exec-aide-listener ListenerVpc: Type: AWS::EC2::VPC Properties: CidrBlock: 10.30.0.0/16 EnableDnsHostnames: true EnableDnsSupport: true Tags: - Key: Name Value: exec-aide ListenerIgw: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: exec-aide ListenerIgwAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref ListenerVpc InternetGatewayId: !Ref ListenerIgw ListenerRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref ListenerVpc ListenerRoute: Type: AWS::EC2::Route DependsOn: ListenerIgwAttachment Properties: RouteTableId: !Ref ListenerRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref ListenerIgw ListenerSubnetRouteTableAssoc: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref ListenerSubnet RouteTableId: !Ref ListenerRouteTable ListenerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: exec-aide listener — outbound only VpcId: !Ref ListenerVpc SecurityGroupEgress: - IpProtocol: "-1" CidrIp: 0.0.0.0/0 ListenerExecutionRole: Type: AWS::IAM::Role Properties: RoleName: exec-aide-listener-execution AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy ListenerTaskRole: Type: AWS::IAM::Role Properties: RoleName: exec-aide-listener-task AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: ExecAideListenerAccess PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:exec-aide/slack-credentials-* - Effect: Allow Action: - ssm:GetParametersByPath - ssm:GetParameter Resource: - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/exec-aide/* - Effect: Allow Action: - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:Query Resource: - !GetAtt ExecAideTable.Arn - !Sub ${ExecAideTable.Arn}/index/* ListenerEcrRepo: Type: AWS::ECR::Repository Properties: RepositoryName: exec-aide-listener ImageScanningConfiguration: ScanOnPush: true LifecyclePolicy: LifecyclePolicyText: | { "rules": [ { "rulePriority": 1, "description": "Keep last 5 images", "selection": { "tagStatus": "any", "countType": "imageCountMoreThan", "countNumber": 5 }, "action": { "type": "expire" } } ] } # ── EventBridge Scheduler (daily digest, DST-aware) ────── DailyDigestSchedule: Type: AWS::Scheduler::Schedule Properties: Name: exec-aide-daily-digest Description: Daily 5 PM ET inbox digest ScheduleExpression: cron(0 17 ? * MON-FRI *) ScheduleExpressionTimezone: America/New_York FlexibleTimeWindow: Mode: "OFF" State: ENABLED Target: Arn: !GetAtt DailyDigestFunction.Arn RoleArn: !GetAtt DailyDigestSchedulerRole.Arn DailyDigestSchedulerRole: Type: AWS::IAM::Role Properties: RoleName: exec-aide-digest-scheduler AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: scheduler.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: InvokeLambda PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt DailyDigestFunction.Arn DailyDigestSchedulePermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref DailyDigestFunction Action: lambda:InvokeFunction Principal: scheduler.amazonaws.com SourceArn: !GetAtt DailyDigestSchedule.Arn Outputs: FetchClassifyFunctionArn: Description: Fetch & classify Lambda ARN Value: !GetAtt FetchClassifyFunction.Arn DailyDigestFunctionArn: Description: Daily digest Lambda ARN Value: !GetAtt DailyDigestFunction.Arn ExecAideTableName: Description: DynamoDB table name Value: !Ref ExecAideTable ListenerEcrRepoUri: Description: ECR repo for the Socket Mode listener Value: !GetAtt ListenerEcrRepo.RepositoryUri EcsClusterName: Description: ECS cluster name Value: !Ref EcsCluster