11import { createHash } from "node:crypto" ;
22import { existsSync , statSync , writeFileSync } from "node:fs" ;
3+ import { homedir } from "node:os" ;
34import { join , resolve } from "node:path" ;
45import { InputValidationError , InvalidEnvironmentError } from "../../errors" ;
56import type { DevEvent , DevRunner , DevServerInput } from "../../handlers/project/dev/types" ;
@@ -10,8 +11,17 @@ import {
1011 type ProcessStreamer ,
1112 type StreamProcessOptions ,
1213} from "../../io" ;
14+ import { DEV_PORTS } from "./port" ;
1315
1416const CONTAINER_TOOLS = [ "docker" , "podman" , "finch" ] as const ;
17+ const AWS_ENV_KEYS = [
18+ "AWS_ACCESS_KEY_ID" ,
19+ "AWS_SECRET_ACCESS_KEY" ,
20+ "AWS_SESSION_TOKEN" ,
21+ "AWS_REGION" ,
22+ "AWS_DEFAULT_REGION" ,
23+ "AWS_PROFILE" ,
24+ ] as const ;
1525const CLEANUP_TIMEOUT_MS = 2_000 ;
1626const DOCKERFILE_NAME = "Dockerfile" ;
1727const CONTAINER_RUNTIME_INSTALL_HINT =
@@ -44,15 +54,21 @@ type ToolAvailable = typeof toolAvailable;
4454type ContainerDevRunnerConfig = {
4555 streamProcess ?: ProcessStreamer ;
4656 toolAvailable ?: ToolAvailable ;
57+ awsDirectory ?: string ;
58+ processEnv ?: NodeJS . ProcessEnv ;
4759} ;
4860
4961export class ContainerDevRunner implements DevRunner {
5062 private readonly streamProcess : ProcessStreamer ;
5163 private readonly toolAvailable : ToolAvailable ;
64+ private readonly awsDirectory : string ;
65+ private readonly processEnv : NodeJS . ProcessEnv ;
5266
5367 constructor ( config : ContainerDevRunnerConfig = { } ) {
5468 this . streamProcess = config . streamProcess ?? streamProcess ;
5569 this . toolAvailable = config . toolAvailable ?? toolAvailable ;
70+ this . awsDirectory = config . awsDirectory ?? join ( homedir ( ) , ".aws" ) ;
71+ this . processEnv = config . processEnv ?? process . env ;
5672 }
5773
5874 public async * run ( input : DevServerInput ) : AsyncGenerator < DevEvent , void > {
@@ -71,6 +87,18 @@ export class ContainerDevRunner implements DevRunner {
7187 throw new InputValidationError ( `container Dockerfile not found: ${ dockerfilePath } ` ) ;
7288 }
7389
90+ const hasAwsCredentials = Boolean (
91+ ( input . env ?. AWS_ACCESS_KEY_ID ?? this . processEnv . AWS_ACCESS_KEY_ID ) &&
92+ ( input . env ?. AWS_SECRET_ACCESS_KEY ?? this . processEnv . AWS_SECRET_ACCESS_KEY ) ,
93+ ) ;
94+ const hasAwsConfig = existsSync ( this . awsDirectory ) ;
95+ if ( ! hasAwsCredentials && ! hasAwsConfig ) {
96+ throw new InputValidationError (
97+ "Unable to resolve AWS credentials for the container. Configure AWS credentials " +
98+ "or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, then retry." ,
99+ ) ;
100+ }
101+
74102 const tool = await this . resolveContainerTool ( input . signal ) ;
75103 input . signal . throwIfAborted ( ) ;
76104 if ( input . runtime . buildContextPath ) {
@@ -116,15 +144,23 @@ export class ContainerDevRunner implements DevRunner {
116144 yield { type : "status" , message : `Building image with ${ tool } ` } ;
117145 yield * this . streamProcess ( buildCommand , buildOptions ) ;
118146
119- const containerPort = portForProtocol ( input . runtime . protocol ) ;
120- const forwardedEnv : Record < string , string > = {
121- ...input . env ,
147+ const containerPort = DEV_PORTS [ input . runtime . protocol ?? "HTTP" ] ;
148+ const forwardedEnv : Record < string , string > = { } ;
149+ for ( const key of AWS_ENV_KEYS ) {
150+ if ( this . processEnv [ key ] ) forwardedEnv [ key ] = this . processEnv [ key ] ;
151+ }
152+ Object . assign ( forwardedEnv , input . env , {
122153 PORT : String ( containerPort ) ,
123154 LOCAL_DEV : "1" ,
124- } ;
155+ } ) ;
125156 if ( input . runtime . protocol === "MCP" ) {
126157 forwardedEnv . FASTMCP_PORT = String ( containerPort ) ;
127158 }
159+ const awsMount = hasAwsConfig ? [ "-v" , `${ this . awsDirectory } :/aws-config:ro` ] : [ ] ;
160+ if ( awsMount . length ) {
161+ forwardedEnv . AWS_CONFIG_FILE = "/aws-config/config" ;
162+ forwardedEnv . AWS_SHARED_CREDENTIALS_FILE = "/aws-config/credentials" ;
163+ }
128164 const envFlags = Object . entries ( forwardedEnv ) . flatMap ( ( [ key , value ] ) => [
129165 "-e" ,
130166 `${ key } =${ value } ` ,
@@ -141,6 +177,7 @@ export class ContainerDevRunner implements DevRunner {
141177 containerName ,
142178 "-p" ,
143179 `127.0.0.1:${ input . port } :${ containerPort } ` ,
180+ ...awsMount ,
144181 ...envFlags ,
145182 imageTag ,
146183 ] ;
@@ -158,6 +195,7 @@ export class ContainerDevRunner implements DevRunner {
158195 containerName ,
159196 "-p" ,
160197 `127.0.0.1:${ input . port } :${ containerPort } ` ,
198+ ...awsMount ,
161199 ...redactedEnvFlags ,
162200 imageTag ,
163201 ] ,
@@ -204,12 +242,6 @@ export class ContainerDevRunner implements DevRunner {
204242 }
205243}
206244
207- function portForProtocol ( protocol : DevServerInput [ "runtime" ] [ "protocol" ] ) : number {
208- if ( protocol === "MCP" ) return 8000 ;
209- if ( protocol === "A2A" ) return 9000 ;
210- return 8080 ;
211- }
212-
213245function isDirectory ( path : string ) : boolean {
214246 try {
215247 return statSync ( path ) . isDirectory ( ) ;
0 commit comments