@@ -105,69 +105,75 @@ export class Ec2ComputeStrategy implements ComputeStrategy {
105105 ] ,
106106 } ) ) ;
107107
108- // 4. Build the boot command (mirrors ECS strategy env vars and Python boot command)
109- const envExports = [
110- `export TASK_ID='${ taskId } '` ,
111- `export REPO_URL='${ String ( payload . repo_url ?? '' ) } '` ,
112- ...( payload . prompt ? [ `export TASK_DESCRIPTION='${ String ( payload . prompt ) . replace ( / ' / g, "'\\''" ) } '` ] : [ ] ) ,
113- ...( payload . issue_number ? [ `export ISSUE_NUMBER='${ String ( payload . issue_number ) } '` ] : [ ] ) ,
114- `export MAX_TURNS='${ String ( payload . max_turns ?? 100 ) } '` ,
115- ...( payload . max_budget_usd !== undefined ? [ `export MAX_BUDGET_USD='${ String ( payload . max_budget_usd ) } '` ] : [ ] ) ,
116- ...( blueprintConfig . model_id ? [ `export ANTHROPIC_MODEL='${ blueprintConfig . model_id } '` ] : [ ] ) ,
117- ...( blueprintConfig . system_prompt_overrides ? [ `export SYSTEM_PROMPT_OVERRIDES='${ blueprintConfig . system_prompt_overrides . replace ( / ' / g, "'\\''" ) } '` ] : [ ] ) ,
118- "export CLAUDE_CODE_USE_BEDROCK='1'" ,
119- ...( payload . github_token_secret_arn ? [ `export GITHUB_TOKEN_SECRET_ARN='${ String ( payload . github_token_secret_arn ) } '` ] : [ ] ) ,
120- ...( payload . memory_id ? [ `export MEMORY_ID='${ String ( payload . memory_id ) } '` ] : [ ] ) ,
121- ] ;
122-
108+ // 4. Build the boot script
109+ // All task data is read from the S3 payload at runtime to avoid shell
110+ // injection — no untrusted values are interpolated into the script.
111+ // Only infrastructure constants (bucket name, ECR URI) are embedded.
123112 const bootScript = [
124113 '#!/bin/bash' ,
125114 'set -euo pipefail' ,
126115 '' ,
116+ '# Derive region from IMDS (SSM does not always set AWS_REGION)' ,
117+ 'export AWS_REGION=$(ec2-metadata --availability-zone | cut -d" " -f2 | sed \'s/.$/\'\'/)\'' ,
118+ 'export AWS_DEFAULT_REGION="$AWS_REGION"' ,
119+ '' ,
120+ '# Resolve instance ID for tag cleanup' ,
121+ 'INSTANCE_ID=$(ec2-metadata -i | cut -d" " -f2)' ,
122+ '' ,
123+ '# Cleanup trap — always retag instance as idle on exit (success, error, or signal)' ,
124+ 'cleanup() {' ,
125+ ' docker system prune -f || true' ,
126+ ' rm -f /tmp/payload.json' ,
127+ ` aws ec2 create-tags --resources "$INSTANCE_ID" --region "$AWS_REGION" --tags Key=bgagent:status,Value=idle || true` ,
128+ ` aws ec2 delete-tags --resources "$INSTANCE_ID" --region "$AWS_REGION" --tags Key=bgagent:task-id || true` ,
129+ '}' ,
130+ 'trap cleanup EXIT' ,
131+ '' ,
127132 '# Fetch payload from S3' ,
128133 `aws s3 cp "s3://${ EC2_PAYLOAD_BUCKET } /${ payloadKey } " /tmp/payload.json` ,
129134 'export AGENT_PAYLOAD=$(cat /tmp/payload.json)' ,
130- '' ,
131- '# Set environment variables' ,
132- ...envExports ,
135+ 'export CLAUDE_CODE_USE_BEDROCK=1' ,
133136 '' ,
134137 '# ECR login and pull' ,
135- `aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $(echo '${ ECR_IMAGE_URI } ' | cut -d/ -f1)` ,
138+ `aws ecr get-login-password --region " $AWS_REGION" | docker login --username AWS --password-stdin $(echo '${ ECR_IMAGE_URI } ' | cut -d/ -f1)` ,
136139 `docker pull '${ ECR_IMAGE_URI } '` ,
137140 '' ,
138- '# Run the agent container' ,
139- 'docker run --rm \\' ,
140- ' -e TASK_ID -e REPO_URL -e CLAUDE_CODE_USE_BEDROCK -e AGENT_PAYLOAD \\' ,
141- ' -e AWS_REGION -e AWS_DEFAULT_REGION \\' ,
142- ` ${ payload . prompt ? '-e TASK_DESCRIPTION ' : '' } ${ payload . issue_number ? '-e ISSUE_NUMBER ' : '' } -e MAX_TURNS \\` ,
143- ` ${ payload . max_budget_usd !== undefined ? '-e MAX_BUDGET_USD ' : '' } ${ blueprintConfig . model_id ? '-e ANTHROPIC_MODEL ' : '' } ${ blueprintConfig . system_prompt_overrides ? '-e SYSTEM_PROMPT_OVERRIDES ' : '' } \\` ,
144- ` ${ payload . github_token_secret_arn ? '-e GITHUB_TOKEN_SECRET_ARN ' : '' } ${ payload . memory_id ? '-e MEMORY_ID ' : '' } \\` ,
145- ` '${ ECR_IMAGE_URI } ' \\` ,
141+ '# Run the agent container — all config is read from AGENT_PAYLOAD inside the container' ,
142+ `docker run --rm -e AGENT_PAYLOAD -e CLAUDE_CODE_USE_BEDROCK -e AWS_REGION -e AWS_DEFAULT_REGION '${ ECR_IMAGE_URI } ' \\` ,
146143 ' python -c \'import json, os, sys; sys.path.insert(0, "/app"); from entrypoint import run_task; p = json.loads(os.environ["AGENT_PAYLOAD"]); r = run_task(repo_url=p.get("repo_url",""), task_description=p.get("prompt",""), issue_number=str(p.get("issue_number","")), github_token=p.get("github_token",""), anthropic_model=p.get("model_id",""), max_turns=int(p.get("max_turns",100)), max_budget_usd=p.get("max_budget_usd"), aws_region=os.environ.get("AWS_REGION",""), task_id=p.get("task_id",""), hydrated_context=p.get("hydrated_context"), system_prompt_overrides=p.get("system_prompt_overrides",""), prompt_version=p.get("prompt_version",""), memory_id=p.get("memory_id",""), task_type=p.get("task_type","new_task"), branch_name=p.get("branch_name",""), pr_number=str(p.get("pr_number",""))); sys.exit(0 if r.get("status")=="success" else 1)\'' ,
147- '' ,
148- '# Cleanup' ,
149- 'docker system prune -f' ,
150- 'rm -f /tmp/payload.json' ,
151- '' ,
152- '# Tag instance back to idle' ,
153- 'INSTANCE_ID=$(ec2-metadata -i | cut -d" " -f2)' ,
154- 'aws ec2 create-tags --resources "$INSTANCE_ID" --tags Key=bgagent:status,Value=idle' ,
155- 'aws ec2 delete-tags --resources "$INSTANCE_ID" --tags Key=bgagent:task-id' ,
156144 ] . join ( '\n' ) ;
157145
158- // 5. Send SSM Run Command
159- const ssmResult = await getSsmClient ( ) . send ( new SendCommandCommand ( {
160- DocumentName : 'AWS-RunShellScript' ,
161- InstanceIds : [ instanceId ] ,
162- Parameters : {
163- commands : [ bootScript ] ,
164- } ,
165- TimeoutSeconds : 32400 , // 9 hours, matches orchestrator max
166- } ) ) ;
146+ // 5. Send SSM Run Command — rollback instance tags on failure
147+ let commandId : string ;
148+ try {
149+ const ssmResult = await getSsmClient ( ) . send ( new SendCommandCommand ( {
150+ DocumentName : 'AWS-RunShellScript' ,
151+ InstanceIds : [ instanceId ] ,
152+ Parameters : {
153+ commands : [ bootScript ] ,
154+ } ,
155+ TimeoutSeconds : 32400 , // 9 hours, matches orchestrator max
156+ } ) ) ;
167157
168- const commandId = ssmResult . Command ?. CommandId ;
169- if ( ! commandId ) {
170- throw new Error ( 'SSM SendCommand returned no CommandId' ) ;
158+ if ( ! ssmResult . Command ?. CommandId ) {
159+ throw new Error ( 'SSM SendCommand returned no CommandId' ) ;
160+ }
161+ commandId = ssmResult . Command . CommandId ;
162+ } catch ( err ) {
163+ // Rollback: retag instance as idle so it's not stuck as busy
164+ try {
165+ await getEc2Client ( ) . send ( new CreateTagsCommand ( {
166+ Resources : [ instanceId ] ,
167+ Tags : [ { Key : 'bgagent:status' , Value : 'idle' } ] ,
168+ } ) ) ;
169+ await getEc2Client ( ) . send ( new DeleteTagsCommand ( {
170+ Resources : [ instanceId ] ,
171+ Tags : [ { Key : 'bgagent:task-id' } ] ,
172+ } ) ) ;
173+ } catch {
174+ logger . warn ( 'Failed to rollback instance tags after dispatch failure' , { instance_id : instanceId , task_id : taskId } ) ;
175+ }
176+ throw err ;
171177 }
172178
173179 logger . info ( 'EC2 SSM command dispatched' , {
0 commit comments