11import { createHash } from "node:crypto" ;
2- import { existsSync , statSync , writeFileSync } from "node:fs" ;
3- import { join , resolve } from "node:path" ;
2+ import { existsSync , realpathSync , statSync , writeFileSync } from "node:fs" ;
3+ import { homedir } from "node:os" ;
4+ import { isAbsolute , join , relative , resolve , sep } from "node:path" ;
45import { InputValidationError , InvalidEnvironmentError } from "../../errors" ;
56import type { DevEvent , DevRunner , DevServerInput } from "../../handlers/project/dev/types" ;
67import {
@@ -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 > {
@@ -65,12 +81,36 @@ export class ContainerDevRunner implements DevRunner {
6581 throw new InputValidationError ( `container build context directory not found: ${ context } ` ) ;
6682 }
6783
84+ const canonicalContext = realpathSync ( context ) ;
85+ const relativeContext = relative ( realpathSync ( input . projectRoot ) , canonicalContext ) ;
86+ if (
87+ relativeContext === ".." ||
88+ relativeContext . startsWith ( `..${ sep } ` ) ||
89+ isAbsolute ( relativeContext )
90+ ) {
91+ throw new InputValidationError (
92+ `container build context must be within the project root: ${ canonicalContext } ` ,
93+ ) ;
94+ }
95+
6896 const dockerfile = input . runtime . dockerfile ?? DOCKERFILE_NAME ;
6997 const dockerfilePath = join ( context , dockerfile ) ;
7098 if ( ! isFile ( dockerfilePath ) ) {
7199 throw new InputValidationError ( `container Dockerfile not found: ${ dockerfilePath } ` ) ;
72100 }
73101
102+ const hasAwsCredentials = Boolean (
103+ ( input . env ?. AWS_ACCESS_KEY_ID ?? this . processEnv . AWS_ACCESS_KEY_ID ) &&
104+ ( input . env ?. AWS_SECRET_ACCESS_KEY ?? this . processEnv . AWS_SECRET_ACCESS_KEY ) ,
105+ ) ;
106+ const hasAwsConfig = existsSync ( this . awsDirectory ) ;
107+ if ( ! hasAwsCredentials && ! hasAwsConfig ) {
108+ throw new InvalidEnvironmentError (
109+ "Unable to resolve AWS credentials for the container. Configure AWS credentials " +
110+ "or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, then retry." ,
111+ ) ;
112+ }
113+
74114 const tool = await this . resolveContainerTool ( input . signal ) ;
75115 input . signal . throwIfAborted ( ) ;
76116 if ( input . runtime . buildContextPath ) {
@@ -116,15 +156,23 @@ export class ContainerDevRunner implements DevRunner {
116156 yield { type : "status" , message : `Building image with ${ tool } ` } ;
117157 yield * this . streamProcess ( buildCommand , buildOptions ) ;
118158
119- const containerPort = portForProtocol ( input . runtime . protocol ) ;
120- const forwardedEnv : Record < string , string > = {
121- ...input . env ,
159+ const containerPort = DEV_PORTS [ input . runtime . protocol ?? "HTTP" ] ;
160+ const forwardedEnv : Record < string , string > = { } ;
161+ for ( const key of AWS_ENV_KEYS ) {
162+ if ( this . processEnv [ key ] ) forwardedEnv [ key ] = this . processEnv [ key ] ;
163+ }
164+ Object . assign ( forwardedEnv , input . env , {
122165 PORT : String ( containerPort ) ,
123166 LOCAL_DEV : "1" ,
124- } ;
167+ } ) ;
125168 if ( input . runtime . protocol === "MCP" ) {
126169 forwardedEnv . FASTMCP_PORT = String ( containerPort ) ;
127170 }
171+ const awsMount = hasAwsConfig ? [ "-v" , `${ this . awsDirectory } :/aws-config:ro` ] : [ ] ;
172+ if ( awsMount . length ) {
173+ forwardedEnv . AWS_CONFIG_FILE = "/aws-config/config" ;
174+ forwardedEnv . AWS_SHARED_CREDENTIALS_FILE = "/aws-config/credentials" ;
175+ }
128176 const envFlags = Object . entries ( forwardedEnv ) . flatMap ( ( [ key , value ] ) => [
129177 "-e" ,
130178 `${ key } =${ value } ` ,
@@ -141,6 +189,7 @@ export class ContainerDevRunner implements DevRunner {
141189 containerName ,
142190 "-p" ,
143191 `127.0.0.1:${ input . port } :${ containerPort } ` ,
192+ ...awsMount ,
144193 ...envFlags ,
145194 imageTag ,
146195 ] ;
@@ -158,6 +207,7 @@ export class ContainerDevRunner implements DevRunner {
158207 containerName ,
159208 "-p" ,
160209 `127.0.0.1:${ input . port } :${ containerPort } ` ,
210+ ...awsMount ,
161211 ...redactedEnvFlags ,
162212 imageTag ,
163213 ] ,
@@ -204,12 +254,6 @@ export class ContainerDevRunner implements DevRunner {
204254 }
205255}
206256
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-
213257function isDirectory ( path : string ) : boolean {
214258 try {
215259 return statSync ( path ) . isDirectory ( ) ;
0 commit comments