Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6e72001
feat(kirocrew): add KiroCrew pack — multi-agent crew gateway on Kiro CLI
royosherove Aug 5, 2026
23f4f9e
fix: address Codex PR review — drop stray '--' in installer args + ad…
royosherove Aug 5, 2026
5086a8a
fix: add kirocrew to telemetry sanitizer in top-level install.sh
royosherove Aug 5, 2026
64e20ca
fix: add kirocrew to pack_default_model + help text in top-level inst…
royosherove Aug 5, 2026
baa100d
fix: use REPO_BRANCH when fetching registry.json fallback (piped inst…
royosherove Aug 5, 2026
ca1c43d
fix: use REPO_BRANCH for git clone + CFN template URL in installer
royosherove Aug 5, 2026
c6d2443
fix: ensure pipx + upstream installer use Python 3.10+ (not system 3.9)
royosherove Aug 5, 2026
1913f89
fix: open port 5476 in SG for kirocrew pack (conditional CFN rule)
royosherove Aug 5, 2026
f93fb2b
fix: address Codex P2 findings — defer template URL + guard systemctl…
royosherove Aug 5, 2026
7d4367e
feat: show dashboard URL + login token in post-install output
royosherove Aug 5, 2026
2ee6e81
fix: bind gateway to 0.0.0.0 (all interfaces) for external access
royosherove Aug 5, 2026
6898111
feat(kirocrew): interactive API key prompt for headless mode
royosherove Aug 7, 2026
dda1ae7
docs: ALB + CloudFront plan for kirocrew dashboard access
royosherove Aug 7, 2026
eb6038b
feat(kirocrew): ALB + CloudFront for dashboard HTTPS access
royosherove Aug 7, 2026
0506272
fix(kirocrew): health check path + post-deploy dashboard.url config
royosherove Aug 7, 2026
ebb7f10
fix: set both KIROCREW_HOST and KIROCREW_BIND for gateway bind
royosherove Aug 7, 2026
520793c
fix(kirocrew): Codex P0+P2 review fixes
royosherove Aug 7, 2026
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
358 changes: 357 additions & 1 deletion deploy/cloudformation/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Derive the second subnet from the configured VPC CIDR

When a KiroCrew deployment uses any supported VpcCidr other than the default 10.0.0.0/16, this hard-coded subnet can fall outside the VPC and CloudFormation rejects it; it can also overlap a valid custom PublicSubnetCidr such as 10.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 👍 / 👎.

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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep generated ALB names within AWS limits

For valid EnvironmentName values longer than 19 characters, ${EnvironmentName}-kirocrew-alb exceeds the 32-character Application Load Balancer name limit, so stack creation fails even though the parameter permits up to 24 characters; the target-group name similarly overflows for longer values. Shorten the generated names or constrain the parameter for KiroCrew deployments.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject direct requests at the ALB listener

In every new-VPC KiroCrew deployment, the ALB security group permits port 80 from 0.0.0.0/0, but this listener forwards all requests without checking the x-origin-verify header. Consequently the generated secret and CloudFront custom header provide no origin protection, and callers can use the exported ALB DNS name to bypass CloudFront's HTTPS policy. Make the default action reject requests and add a forwarding rule that matches the secret header, or otherwise restrict ingress to CloudFront.

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' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid setting a disallowed CloudFront header

For every new-VPC KiroCrew dashboard request, this viewer-request function adds x-forwarded-proto, which CloudFront classifies as a disallowed edge-function header. CloudFront therefore fails request validation and returns a 502 before forwarding to the ALB, leaving the only dashboard endpoint exposed by the stack unusable. Pass protocol information through an allowed header (such as CloudFront-Forwarded-Proto) and configure the gateway to consume it instead.

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
# --------------------------------------------------------------------------
Expand All @@ -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'
Expand Down Expand Up @@ -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)
# --------------------------------------------------------------------------
# --------------------------------------------------------------------------
Expand Down Expand Up @@ -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

Loading