-
Notifications
You must be signed in to change notification settings - Fork 9
feat(kirocrew): add KiroCrew pack — multi-agent crew gateway on Kiro CLI #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6e72001
23f4f9e
5086a8a
64e20ca
baa100d
ca1c43d
c6d2443
1913f89
f93fb2b
7d4367e
2ee6e81
6898111
dda1ae7
eb6038b
0506272
ebb7f10
520793c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -128,7 +128,8 @@ Parameters: | |
| - codex-cli | ||
| - roundhouse | ||
| - troika | ||
| Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'roundhouse' is the pi-based agent with Telegram. 'hermes' is the NousResearch CLI agent (lighter). 'kiro-cli' and 'codex-cli' are experimental. 'troika' installs OpenClaw/Hermes + Claude Code + Codex CLI on one instance (experimental)." | ||
| - kirocrew | ||
| Description: "Agent pack to deploy. 'openclaw' is the stateful AI agent with 24/7 gateway (recommended). 'roundhouse' is the pi-based agent with Telegram. 'hermes' is the NousResearch CLI agent (lighter). 'kiro-cli' and 'codex-cli' are experimental. 'troika' installs OpenClaw/Hermes + Claude Code + Codex CLI on one instance (experimental). 'kirocrew' is the multi-agent crew gateway on Kiro CLI (experimental)." | ||
|
|
||
| ProfileName: | ||
| Type: String | ||
|
|
@@ -381,6 +382,13 @@ Conditions: | |
| IsPersonalAssistant: !Equals [!Ref ProfileName, 'personal_assistant'] | ||
| RunSecurityServices: !Not [!Condition IsPersonalAssistant] | ||
| RunBedrockForm: !Equals [!Ref EnableBedrockForm, 'true'] | ||
| IsKiroCrew: !Equals [!Ref PackName, 'kirocrew'] | ||
| KiroCrewWithNewVpc: !And | ||
| - !Condition IsKiroCrew | ||
| - !Condition CreateNewVpc | ||
| KiroCrewExistingVpc: !And | ||
| - !Condition IsKiroCrew | ||
| - !Not [!Condition CreateNewVpc] | ||
|
|
||
| # ============================================================================ | ||
| # RESOURCES | ||
|
|
@@ -479,6 +487,187 @@ Resources: | |
| SubnetId: !Ref PublicSubnet | ||
| RouteTableId: !Ref PublicRouteTable | ||
|
|
||
| # -------------------------------------------------------------------------- | ||
| # KiroCrew: Second subnet (ALB requires 2 AZs) | ||
| # -------------------------------------------------------------------------- | ||
| KiroCrewSubnet2: | ||
| Type: AWS::EC2::Subnet | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| VpcId: !Ref VPC | ||
| CidrBlock: '10.0.2.0/24' | ||
| MapPublicIpOnLaunch: true | ||
| AvailabilityZone: !Select [1, !GetAZs ''] | ||
| Tags: | ||
| - Key: Name | ||
| Value: !Sub '${EnvironmentName}-public-2' | ||
| - Key: loki:managed | ||
| Value: 'true' | ||
| - Key: loki:watermark | ||
| Value: !Ref LokiWatermark | ||
|
|
||
| KiroCrewSubnet2RouteTableAssociation: | ||
| Type: AWS::EC2::SubnetRouteTableAssociation | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| SubnetId: !Ref KiroCrewSubnet2 | ||
| RouteTableId: !Ref PublicRouteTable | ||
|
|
||
| # -------------------------------------------------------------------------- | ||
| # KiroCrew: ALB + Target Group + CloudFront | ||
| # -------------------------------------------------------------------------- | ||
| KiroCrewALBSecurityGroup: | ||
| Type: AWS::EC2::SecurityGroup | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| GroupName: !Sub '${AWS::StackName}-kirocrew-alb-sg' | ||
| GroupDescription: ALB for KiroCrew dashboard (CloudFront origin-facing only) | ||
| VpcId: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] | ||
| SecurityGroupIngress: | ||
| - IpProtocol: tcp | ||
| FromPort: 80 | ||
| ToPort: 80 | ||
| CidrIp: '0.0.0.0/0' | ||
| Description: HTTP from CloudFront (prefix list not available in all regions — origin-verify header enforces) | ||
| SecurityGroupEgress: | ||
| - IpProtocol: '-1' | ||
| CidrIp: '0.0.0.0/0' | ||
| Description: All outbound | ||
| Tags: | ||
| - Key: Name | ||
| Value: !Sub '${EnvironmentName}-kirocrew-alb-sg' | ||
| - Key: loki:managed | ||
| Value: 'true' | ||
| - Key: loki:watermark | ||
| Value: !Ref LokiWatermark | ||
|
|
||
| KiroCrewALB: | ||
| Type: AWS::ElasticLoadBalancingV2::LoadBalancer | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| Name: !Sub '${EnvironmentName}-kirocrew-alb' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For valid Useful? React with 👍 / 👎. |
||
| Scheme: internet-facing | ||
| Type: application | ||
| Subnets: | ||
| - !If [CreateNewVpc, !Ref PublicSubnet, !Ref ExistingSubnetId] | ||
| - !If [CreateNewVpc, !Ref KiroCrewSubnet2, !Ref ExistingSubnetId] | ||
| SecurityGroups: | ||
| - !Ref KiroCrewALBSecurityGroup | ||
| Tags: | ||
| - Key: loki:managed | ||
| Value: 'true' | ||
| - Key: loki:watermark | ||
| Value: !Ref LokiWatermark | ||
| - Key: loki:pack | ||
| Value: kirocrew | ||
|
|
||
| KiroCrewTargetGroup: | ||
| Type: AWS::ElasticLoadBalancingV2::TargetGroup | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| Name: !Sub '${EnvironmentName}-kirocrew-tg' | ||
| Port: 5476 | ||
| Protocol: HTTP | ||
| VpcId: !If [CreateNewVpc, !Ref VPC, !Ref ExistingVpcId] | ||
| TargetType: instance | ||
| Targets: | ||
| - Id: !Ref Instance | ||
| HealthCheckPath: /api/health | ||
| HealthCheckIntervalSeconds: 30 | ||
| HealthCheckTimeoutSeconds: 10 | ||
| HealthyThresholdCount: 2 | ||
| UnhealthyThresholdCount: 3 | ||
| Tags: | ||
| - Key: loki:managed | ||
| Value: 'true' | ||
|
|
||
| KiroCrewHTTPListener: | ||
| Type: AWS::ElasticLoadBalancingV2::Listener | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| LoadBalancerArn: !Ref KiroCrewALB | ||
| Port: 80 | ||
| Protocol: HTTP | ||
| DefaultActions: | ||
| - Type: forward | ||
| TargetGroupArn: !Ref KiroCrewTargetGroup | ||
|
Comment on lines
+591
to
+593
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In every new-VPC KiroCrew deployment, the ALB security group permits port 80 from Useful? React with 👍 / 👎. |
||
|
|
||
| KiroCrewOriginVerifySecret: | ||
| Type: AWS::SecretsManager::Secret | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| Name: !Sub '/lowkey/${EnvironmentName}/kirocrew-origin-verify' | ||
| Description: Origin verify header value — CloudFront injects, ALB validates | ||
| GenerateSecretString: | ||
| ExcludePunctuation: true | ||
| PasswordLength: 32 | ||
| Tags: | ||
| - Key: loki:managed | ||
| Value: 'true' | ||
| - Key: loki:pack | ||
| Value: kirocrew | ||
|
|
||
| KiroCrewProtoHeaderFunction: | ||
| Type: AWS::CloudFront::Function | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| Name: !Sub '${EnvironmentName}-kirocrew-proto' | ||
| AutoPublish: true | ||
| FunctionConfig: | ||
| Comment: 'Inject X-Forwarded-Proto: https so backend cookies get Secure flag' | ||
| Runtime: cloudfront-js-2.0 | ||
| FunctionCode: | | ||
| function handler(event) { | ||
| event.request.headers['x-forwarded-proto'] = { value: 'https' }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For every new-VPC KiroCrew dashboard request, this viewer-request function adds Useful? React with 👍 / 👎. |
||
| return event.request; | ||
| } | ||
|
|
||
| KiroCrewDistribution: | ||
| Type: AWS::CloudFront::Distribution | ||
| Condition: KiroCrewWithNewVpc | ||
| DependsOn: KiroCrewALB | ||
| Properties: | ||
| DistributionConfig: | ||
| Enabled: true | ||
| Comment: !Sub 'KiroCrew dashboard (${EnvironmentName})' | ||
| HttpVersion: http2and3 | ||
| DefaultCacheBehavior: | ||
| TargetOriginId: kirocrew-alb-origin | ||
| ViewerProtocolPolicy: redirect-to-https | ||
| AllowedMethods: | ||
| - GET | ||
| - HEAD | ||
| - OPTIONS | ||
| - PUT | ||
| - POST | ||
| - PATCH | ||
| - DELETE | ||
| CachePolicyId: '4135ea2d-6df8-44a3-9df3-4b5a84be39ad' # CachingDisabled | ||
| OriginRequestPolicyId: '216adef6-5c7f-47e4-b989-5492eafa07d3' # AllViewer | ||
| FunctionAssociations: | ||
| - EventType: viewer-request | ||
| FunctionARN: !GetAtt KiroCrewProtoHeaderFunction.FunctionARN | ||
| Origins: | ||
| - Id: kirocrew-alb-origin | ||
| DomainName: !GetAtt KiroCrewALB.DNSName | ||
| CustomOriginConfig: | ||
| OriginProtocolPolicy: http-only | ||
| HTTPPort: 80 | ||
| OriginSSLProtocols: | ||
| - TLSv1.2 | ||
| OriginCustomHeaders: | ||
| - HeaderName: x-origin-verify | ||
| HeaderValue: !Sub '{{resolve:secretsmanager:${KiroCrewOriginVerifySecret}}}' | ||
| ViewerCertificate: | ||
| CloudFrontDefaultCertificate: true | ||
| Tags: | ||
| - Key: loki:managed | ||
| Value: 'true' | ||
| - Key: loki:watermark | ||
| Value: !Ref LokiWatermark | ||
| - Key: loki:pack | ||
| Value: kirocrew | ||
|
|
||
| # -------------------------------------------------------------------------- | ||
| # Security Group | ||
| # -------------------------------------------------------------------------- | ||
|
|
@@ -494,6 +683,22 @@ Resources: | |
| ToPort: 22 | ||
| CidrIp: !Ref SSHAllowedCidr | ||
| Description: SSH access | ||
| - !If | ||
| - KiroCrewWithNewVpc | ||
| - IpProtocol: tcp | ||
| FromPort: 5476 | ||
| ToPort: 5476 | ||
| SourceSecurityGroupId: !Ref KiroCrewALBSecurityGroup | ||
| Description: KiroCrew gateway from ALB only | ||
| - !Ref 'AWS::NoValue' | ||
| - !If | ||
| - KiroCrewExistingVpc | ||
| - IpProtocol: tcp | ||
| FromPort: 5476 | ||
| ToPort: 5476 | ||
| CidrIp: '0.0.0.0/0' | ||
| Description: KiroCrew gateway (direct access, existing VPC without ALB) | ||
| - !Ref 'AWS::NoValue' | ||
| SecurityGroupEgress: | ||
| - IpProtocol: '-1' | ||
| CidrIp: '0.0.0.0/0' | ||
|
|
@@ -1047,6 +1252,147 @@ Resources: | |
| EnableAccessAnalyzer: !Ref EnableAccessAnalyzer | ||
| EnableConfigRecorder: !Ref EnableConfigRecorder | ||
|
|
||
| # -------------------------------------------------------------------------- | ||
| # KiroCrew: Post-deploy dashboard.url configuration via SSM RunCommand | ||
| # -------------------------------------------------------------------------- | ||
| KiroCrewDashboardConfigRole: | ||
| Type: AWS::IAM::Role | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| RoleName: !Sub '${EnvironmentName}-kirocrew-dashcfg-role' | ||
| AssumeRolePolicyDocument: | ||
| Version: '2012-10-17' | ||
| Statement: | ||
| - Effect: Allow | ||
| Principal: | ||
| Service: lambda.amazonaws.com | ||
| Action: sts:AssumeRole | ||
| ManagedPolicyArns: | ||
| - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole | ||
| Policies: | ||
| - PolicyName: SSMRunCommand | ||
| PolicyDocument: | ||
| Version: '2012-10-17' | ||
| Statement: | ||
| - Effect: Allow | ||
| Action: | ||
| - ssm:SendCommand | ||
| - ssm:GetCommandInvocation | ||
| Resource: '*' | ||
|
|
||
| KiroCrewDashboardConfigFunction: | ||
| Type: AWS::Lambda::Function | ||
| Condition: KiroCrewWithNewVpc | ||
| Properties: | ||
| FunctionName: !Sub '${EnvironmentName}-kirocrew-dashcfg' | ||
| Runtime: python3.12 | ||
| Handler: index.handler | ||
| Timeout: 300 | ||
| Role: !GetAtt KiroCrewDashboardConfigRole.Arn | ||
| Code: | ||
| ZipFile: | | ||
| import json, urllib.request, boto3, time | ||
|
|
||
| def send_response(event, context, status, reason='', data={}): | ||
| reason_str = (reason or f'See CW: {context.log_stream_name}')[:256] | ||
| phys_id = (context.log_stream_name or 'custom-resource')[-64:] | ||
| safe_data = {k: str(v)[:128] for k, v in (data or {}).items()} | ||
| body = json.dumps({ | ||
| 'Status': status, 'Reason': reason_str, | ||
| 'PhysicalResourceId': phys_id, | ||
| 'StackId': event['StackId'], | ||
| 'RequestId': event['RequestId'], | ||
| 'LogicalResourceId': event['LogicalResourceId'], | ||
| 'Data': safe_data if len(json.dumps(safe_data)) < 1024 else {} | ||
| }).encode() | ||
| req = urllib.request.Request(event['ResponseURL'], data=body, | ||
| headers={'Content-Type': 'application/json', 'Content-Length': len(body)}, | ||
| method='PUT') | ||
| urllib.request.urlopen(req) | ||
|
|
||
| def handler(event, context): | ||
| print(f"[INFO] Event: {json.dumps(event)}") | ||
| if event['RequestType'] == 'Delete': | ||
| send_response(event, context, 'SUCCESS', 'Delete is a no-op') | ||
| return | ||
|
|
||
| props = event.get('ResourceProperties', {}) | ||
| instance_id = props.get('InstanceId', '') | ||
| dashboard_url = props.get('DashboardUrl', '') | ||
| region = props.get('Region', 'us-east-1') | ||
|
|
||
| if not instance_id or not dashboard_url: | ||
| send_response(event, context, 'FAILED', 'Missing InstanceId or DashboardUrl') | ||
| return | ||
|
|
||
| ssm = boto3.client('ssm', region_name=region) | ||
| # Patch config.json with dashboard.url and restart gateway | ||
| command = f"""#!/bin/bash | ||
| set -e | ||
| CONFIG_FILE="/home/ec2-user/.kiro/crew/config.json" | ||
| mkdir -p "$(dirname "$CONFIG_FILE")" | ||
| if [ -f "$CONFIG_FILE" ]; then | ||
| # Merge dashboard.url into existing config | ||
| python3 -c " | ||
| import json, sys | ||
| cfg = json.load(open('$CONFIG_FILE')) | ||
| cfg.setdefault('dashboard', {{}})['url'] = '{dashboard_url}' | ||
| json.dump(cfg, open('$CONFIG_FILE', 'w'), indent=2) | ||
| print('Updated dashboard.url in config.json') | ||
| " | ||
| else | ||
| echo '{{"dashboard": {{"url": "{dashboard_url}"}}}}' > "$CONFIG_FILE" | ||
| chown ec2-user:ec2-user "$CONFIG_FILE" | ||
| fi | ||
| # Restart gateway to pick up new config | ||
| if systemctl is-active --quiet kirocrew-gateway 2>/dev/null; then | ||
| systemctl restart kirocrew-gateway | ||
| echo 'Restarted kirocrew-gateway' | ||
| fi | ||
| """ | ||
| try: | ||
| resp = ssm.send_command( | ||
| InstanceIds=[instance_id], | ||
| DocumentName='AWS-RunShellScript', | ||
| Parameters={'commands': [command]}, | ||
| TimeoutSeconds=120 | ||
| ) | ||
| cmd_id = resp['Command']['CommandId'] | ||
| # Wait for completion (tolerate InvocationDoesNotExist during agent pickup) | ||
| result = None | ||
| for _ in range(30): | ||
| time.sleep(5) | ||
| try: | ||
| result = ssm.get_command_invocation( | ||
| CommandId=cmd_id, InstanceId=instance_id) | ||
| except ssm.exceptions.InvocationDoesNotExist: | ||
| continue | ||
| if result['Status'] in ('Success', 'Failed', 'TimedOut', 'Cancelled'): | ||
| break | ||
| if result is None: | ||
| send_response(event, context, 'FAILED', 'SSM command timed out waiting for invocation') | ||
| return | ||
| if result['Status'] == 'Success': | ||
| send_response(event, context, 'SUCCESS', 'dashboard.url configured', | ||
| {'DashboardUrl': dashboard_url}) | ||
| else: | ||
| send_response(event, context, 'FAILED', | ||
| f"SSM command {result['Status']}: {result.get('StandardErrorContent', '')[:200]}") | ||
| except Exception as e: | ||
| send_response(event, context, 'FAILED', str(e)[:256]) | ||
|
|
||
| KiroCrewDashboardConfigResource: | ||
| Type: Custom::KiroCrewDashboardConfig | ||
| Condition: KiroCrewWithNewVpc | ||
| DependsOn: | ||
| - Instance | ||
| - KiroCrewDistribution | ||
| Properties: | ||
| ServiceToken: !GetAtt KiroCrewDashboardConfigFunction.Arn | ||
| InstanceId: !Ref Instance | ||
| DashboardUrl: !Sub 'https://${KiroCrewDistribution.DomainName}' | ||
| Region: !Ref 'AWS::Region' | ||
|
|
||
| # SSM Session Manager Preferences (auto-login as ec2-user with welcome) | ||
| # -------------------------------------------------------------------------- | ||
| # -------------------------------------------------------------------------- | ||
|
|
@@ -1230,3 +1576,13 @@ Outputs: | |
| Description: "Deployed permission profile" | ||
| Value: !Ref ProfileName | ||
|
|
||
| KiroCrewDashboardUrl: | ||
| Condition: KiroCrewWithNewVpc | ||
| Description: KiroCrew dashboard URL (CloudFront HTTPS) | ||
| Value: !Sub 'https://${KiroCrewDistribution.DomainName}' | ||
|
|
||
| KiroCrewALBDns: | ||
| Condition: KiroCrewWithNewVpc | ||
| Description: KiroCrew ALB DNS name (do not access directly — use CloudFront) | ||
| Value: !GetAtt KiroCrewALB.DNSName | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a KiroCrew deployment uses any supported
VpcCidrother than the default10.0.0.0/16, this hard-coded subnet can fall outside the VPC and CloudFormation rejects it; it can also overlap a valid customPublicSubnetCidrsuch as10.0.2.0/24. Derive or parameterize the second subnet CIDR and validate that both public subnets are distinct members of the selected VPC.Useful? React with 👍 / 👎.