Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/setup-github-projects_Version3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
while read -r issue_num; do
[ -z "$issue_num" ] && continue
ISSUE_URL="https://github.com/$REPO/issues/$issue_num"
err=$(gh project item-add "$PROJECT_ID" --url "$ISSUE_URL" 2>&1) || {
err=$(gh project item-add "$PROJECT_ID" --owner MikePfunk28 --url "$ISSUE_URL" 2>&1) || {
echo "Error adding issue $issue_num to project $PROJECT_ID: $err" >&2
failures=$((failures+1))
}
Expand All @@ -58,10 +58,10 @@ jobs:

if [ "${{ github.event_name }}" = "issues" ]; then
ISSUE_URL="https://github.com/${REPO}/issues/${{ github.event.issue.number }}"
out=$(gh project item-add "$PROJECT_ID" --url "$ISSUE_URL" 2>&1) || { echo "Error adding issue $ISSUE_URL to project: $out" >&2; exit 1; }
out=$(gh project item-add "$PROJECT_ID" --owner MikePfunk28 --url "$ISSUE_URL" 2>&1) || { echo "Error adding issue $ISSUE_URL to project: $out" >&2; exit 1; }
elif [ "${{ github.event_name }}" = "pull_request" ]; then
URL="https://github.com/${REPO}/pull/${{ github.event.pull_request.number }}"
out=$(gh project item-add "$PROJECT_ID" --url "$URL" 2>&1) || { echo "Error adding PR $URL to project: $out" >&2; exit 1; }
out=$(gh project item-add "$PROJECT_ID" --owner MikePfunk28 --url "$URL" 2>&1) || { echo "Error adding PR $URL to project: $out" >&2; exit 1; }
else
echo "Unsupported event: ${{ github.event_name }}" >&2
exit 1
Expand Down
213 changes: 45 additions & 168 deletions cloudformation/user-onboarding-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ Parameters:
Description: The AWS Account ID of the Agent Builder Platform
AllowedPattern: '[0-9]{12}'
ConstraintDescription: Must be a valid 12-digit AWS Account ID

UserIdentifier:
Type: String
Description: Your email or unique identifier (used as External ID for security)
Default: ''

ProjectName:
Type: String
Description: Project name for resource tagging
Expand All @@ -36,121 +36,35 @@ Metadata:
default: 'Project Name'

Resources:
# VPC for agent deployment
AgentVPC:
Type: AWS::EC2::VPC
# S3 Bucket for agent artifacts
AgentArtifactsBucket:
Type: AWS::S3::Bucket
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
BucketName: !Sub '${ProjectName}-${AWS::AccountId}-artifacts'
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
Tags:
- Key: Name
Value: !Sub '${ProjectName}-vpc'
Value: !Sub '${ProjectName}-artifacts'
- Key: ManagedBy
Value: 'CloudFormation'

# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub '${ProjectName}-igw'

AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref AgentVPC
InternetGatewayId: !Ref InternetGateway

# Public Subnet
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref AgentVPC
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: true
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${ProjectName}-public-subnet'

# Route Table
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref AgentVPC
Tags:
- Key: Name
Value: !Sub '${ProjectName}-public-rt'

PublicRoute:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway

SubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable

# Security Group
AgentSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub '${ProjectName}-agent-sg'
GroupDescription: Security group for Agent Builder agents
VpcId: !Ref AgentVPC
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${ProjectName}-agent-sg'

# ECS Cluster
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub '${ProjectName}-cluster'
CapacityProviders:
- FARGATE
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 1
Tags:
- Key: Name
Value: !Sub '${ProjectName}-cluster'

# CloudWatch Log Group
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/aws/${ProjectName}/agents'
LogGroupName: !Sub '/aws/agentcore/${ProjectName}/agents'
RetentionInDays: 7

# IAM Role for Fargate Task Execution
FargateExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ProjectName}-fargate-execution-role'
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
Tags:
- Key: Name
Value: !Sub '${ProjectName}-fargate-execution-role'

# Cross-Account IAM Role
CrossAccountRole:
Type: AWS::IAM::Role
Expand Down Expand Up @@ -179,41 +93,40 @@ Resources:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: ECSPermissions
- Sid: BedrockPermissions
Effect: Allow
Action:
- ecs:CreateCluster
- ecs:RegisterTaskDefinition
- ecs:RunTask
- ecs:StopTask
- ecs:DescribeTasks
- ecs:DescribeTaskDefinition
- ecs:ListTasks
- bedrock:CreateAgent
- bedrock:CreateAgentActionGroup
- bedrock:InvokeAgent
- bedrock:InvokeModel
- bedrock:InvokeModelWithResponseStream
- bedrock:GetAgent
- bedrock:ListAgents
- bedrock:UpdateAgent
- bedrock:DeleteAgent
Resource: '*'
- Sid: ECRPermissions

- Sid: S3Permissions
Effect: Allow
Action:
- ecr:CreateRepository
- ecr:GetAuthorizationToken
- ecr:BatchCheckLayerAvailability
- ecr:GetDownloadUrlForLayer
- ecr:BatchGetImage
- ecr:PutImage
- ecr:InitiateLayerUpload
- ecr:UploadLayerPart
- ecr:CompleteLayerUpload
Resource: '*'

- s3:GetObject
- s3:PutObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- !GetAtt AgentArtifactsBucket.Arn
- !Sub '${AgentArtifactsBucket.Arn}/*'

- Sid: LogsPermissions
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- logs:DescribeLogStreams
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/${ProjectName}/*'
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/agentcore/${ProjectName}/*'

- Sid: IAMPermissions
Effect: Allow
Action:
Expand All @@ -222,24 +135,6 @@ Resources:
- iam:PassRole
- iam:GetRole
Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/${ProjectName}-*'

- Sid: VPCPermissions
Effect: Allow
Action:
- ec2:DescribeVpcs
- ec2:DescribeSubnets
- ec2:DescribeSecurityGroups
- ec2:CreateSecurityGroup
- ec2:AuthorizeSecurityGroupIngress
- ec2:AuthorizeSecurityGroupEgress
Resource: '*'

- Sid: BedrockPermissions
Effect: Allow
Action:
- bedrock:InvokeModel
- bedrock:InvokeModelWithResponseStream
Resource: '*'
Roles:
- !Ref CrossAccountRole

Expand All @@ -262,29 +157,11 @@ Outputs:
Export:
Name: !Sub '${AWS::StackName}-Region'

ECSClusterName:
Description: ECS Cluster name for agent deployment
Value: !Ref ECSCluster
Export:
Name: !Sub '${AWS::StackName}-ECSCluster'

VPCId:
Description: VPC ID
Value: !Ref AgentVPC
Export:
Name: !Sub '${AWS::StackName}-VPCId'

SubnetId:
Description: Public Subnet ID
Value: !Ref PublicSubnet
Export:
Name: !Sub '${AWS::StackName}-SubnetId'

SecurityGroupId:
Description: Security Group ID
Value: !Ref AgentSecurityGroup
ArtifactsBucketName:
Description: S3 bucket for agent artifacts
Value: !Ref AgentArtifactsBucket
Export:
Name: !Sub '${AWS::StackName}-SecurityGroupId'
Name: !Sub '${AWS::StackName}-ArtifactsBucket'

SetupInstructions:
Description: Next steps
Expand Down
31 changes: 31 additions & 0 deletions convex/agentBuilderWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,15 @@ export const executeWorkflowStage = action( {
throw new Error( gateResult.reason );
}

// Rate limit: prevent burst abuse per user
const { checkRateLimit, buildTierRateLimitConfig } = await import( "./rateLimiter" );
const { getTierConfig } = await import( "./lib/tierConfig" );
const rlCfg = buildTierRateLimitConfig( getTierConfig( gateResult.tier ).maxConcurrentTests, "agentExecution" );
const rlResult = await checkRateLimit( ctx, String( gateResult.userId ), "agentExecution", rlCfg );
if ( !rlResult.allowed ) {
throw new Error( rlResult.reason ?? "Rate limit exceeded. Please try again later." );
}

const stage = WORKFLOW_STAGES[args.stage as keyof typeof WORKFLOW_STAGES];
if ( !stage ) {
throw new Error( `Invalid workflow stage: ${args.stage}` );
Expand Down Expand Up @@ -464,6 +473,17 @@ export const executeCompleteWorkflow = action( {
throw new Error( gateResult.reason );
}

// Rate limit: prevent burst abuse per user
{
const { checkRateLimit, buildTierRateLimitConfig } = await import( "./rateLimiter" );
const { getTierConfig } = await import( "./lib/tierConfig" );
const rlCfg = buildTierRateLimitConfig( getTierConfig( gateResult.tier ).maxConcurrentTests, "agentExecution" );
const rlResult = await checkRateLimit( ctx, String( gateResult.userId ), "agentExecution", rlCfg );
if ( !rlResult.allowed ) {
throw new Error( rlResult.reason ?? "Rate limit exceeded. Please try again later." );
}
}
Comment on lines +476 to +485
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Rate-limit double-counting will break multi-stage workflows for lower tiers.

executeCompleteWorkflow checks the rate limit once here (lines 476–485), then calls executeWorkflowStage for each of the 9 stages (line 507), and each of those calls runs its own rate-limit check (lines 281–288). That's 10 rate-limit entries recorded for a single logical workflow execution. With freemium's maxConcurrentTests = 1 yielding maxPerMinute = 2, the workflow will fail at stage 2.

Consider either:

  1. Skipping the per-stage rate-limit check when called from executeCompleteWorkflow (e.g., pass an internal flag or use an internal action variant).
  2. Removing the rate-limit check from executeCompleteWorkflow itself and relying on the per-stage checks alone (accepting the 9-entry cost).
  3. Performing a single pre-flight check here and bypassing the per-stage checks.
🤖 Prompt for AI Agents
In `@convex/agentBuilderWorkflow.ts` around lines 476 - 485,
executeCompleteWorkflow performs a global rate-check but then calls
executeWorkflowStage 9 times, causing double counting; modify
executeWorkflowStage to accept an options flag (e.g., internal:boolean) that,
when true, skips its own checkRateLimit call, and then change
executeCompleteWorkflow to pass internal=true for each stage invocation; keep
the existing buildTierRateLimitConfig/getTierConfig/checkRateLimit usage in
executeCompleteWorkflow (so pre-flight remains) and only short-circuit the
per-stage rate-limit logic inside executeWorkflowStage by checking the new
internal flag before calling checkRateLimit.


const workflowResults: Array<{
stage: string;
output: string;
Expand Down Expand Up @@ -539,6 +559,17 @@ export const streamWorkflowExecution = action( {
throw new Error( gateResult.reason );
}

// Rate limit: prevent burst abuse per user
{
const { checkRateLimit, buildTierRateLimitConfig } = await import( "./rateLimiter" );
const { getTierConfig } = await import( "./lib/tierConfig" );
const rlCfg = buildTierRateLimitConfig( getTierConfig( gateResult.tier ).maxConcurrentTests, "agentExecution" );
const rlResult = await checkRateLimit( ctx, String( gateResult.userId ), "agentExecution", rlCfg );
if ( !rlResult.allowed ) {
throw new Error( rlResult.reason ?? "Rate limit exceeded. Please try again later." );
}
}

const { resolveBedrockModelId } = await import( "./modelRegistry.js" );
const resolvedModelId = resolveBedrockModelId( effectiveModelId );

Expand Down
Loading