@@ -49,6 +49,10 @@ const upsertProjectIndex = (projectId: string, agentId: string): void => {
4949}
5050
5151const shellEscape = ( value : string ) : string => `'${ value . replaceAll ( "'" , "'\\''" ) } '`
52+ const agentEnvKeyPattern = / ^ [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * $ / u
53+ const simpleEnvAssignmentPattern = / ^ [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * = [ ^ \s ] + $ / u
54+
55+ const agentHome = ( sshUser : string ) : string => `/home/${ sshUser } `
5256
5357const sourceLabel = ( request : CreateAgentRequest ) : string =>
5458 request . label ?. trim ( ) . length ? request . label . trim ( ) : request . provider
@@ -81,31 +85,97 @@ export const buildCommand = (request: CreateAgentRequest): string => {
8185 return args . length === 0 ? base : `${ base } ${ args . join ( " " ) } `
8286}
8387
84- const buildAgentScript = (
88+ const buildEnvExports = (
89+ envEntries : ReadonlyArray < { readonly key : string ; readonly value : string } >
90+ ) : string => envEntries
91+ . map ( ( { key, value } ) => {
92+ if ( ! agentEnvKeyPattern . test ( key ) ) {
93+ throw new ApiBadRequestError ( { message : `Invalid agent env key: ${ key } ` } )
94+ }
95+ return `export ${ key } =${ shellEscape ( value ) } `
96+ } )
97+ . join ( "\n" )
98+
99+ const execLine = ( command : string ) : string => {
100+ const parts = command . trim ( ) . split ( / \s + / u)
101+ const firstCommandIndex = parts . findIndex ( ( part ) => ! simpleEnvAssignmentPattern . test ( part ) )
102+
103+ return firstCommandIndex > 0
104+ ? `exec env ${ parts . slice ( 0 , firstCommandIndex ) . join ( " " ) } ${ parts . slice ( firstCommandIndex ) . join ( " " ) } `
105+ : `exec ${ command } `
106+ }
107+
108+ export const buildAgentScript = (
85109 sessionId : string ,
86110 cwd : string ,
111+ sshUser : string ,
112+ codexHome : string ,
87113 envEntries : ReadonlyArray < { readonly key : string ; readonly value : string } > ,
88114 command : string
89115) : string => {
90116 const pidFile = `/tmp/docker-git-agent-${ sessionId } .pid`
91- const exports = envEntries
92- . map ( ( { key , value } ) => `export ${ key } = ${ shellEscape ( value ) } ` )
93- . join ( "\n" )
117+ const home = agentHome ( sshUser )
118+ const sshEnvPath = ` ${ home } /.ssh/environment`
119+ const exports = buildEnvExports ( envEntries )
94120
95121 return [
96- "set -euo pipefail" ,
122+ "set -eo pipefail" ,
97123 `PID_FILE=${ shellEscape ( pidFile ) } ` ,
98124 "cleanup() { rm -f \"$PID_FILE\"; }" ,
99125 "trap cleanup EXIT" ,
100126 "echo $$ > \"$PID_FILE\"" ,
127+ `export HOME=${ shellEscape ( home ) } ` ,
128+ `export USER=${ shellEscape ( sshUser ) } ` ,
129+ `export LOGNAME=${ shellEscape ( sshUser ) } ` ,
130+ `export CODEX_HOME=${ shellEscape ( codexHome ) } ` ,
131+ "export DOCKER_GIT_RTK_ENABLE=\"${DOCKER_GIT_RTK_ENABLE:-1}\"" ,
132+ "if [ -f /etc/profile ]; then . /etc/profile >/dev/null 2>&1 || true; fi" ,
133+ `if [ -f ${ shellEscape ( sshEnvPath ) } ]; then` ,
134+ " set -a" ,
135+ ` . ${ shellEscape ( sshEnvPath ) } >/dev/null 2>&1 || true` ,
136+ " set +a" ,
137+ "fi" ,
138+ "if [ -f /run/docker-git/agent-env.sh ]; then . /run/docker-git/agent-env.sh >/dev/null 2>&1 || true; fi" ,
139+ `export HOME=${ shellEscape ( home ) } ` ,
140+ `export USER=${ shellEscape ( sshUser ) } ` ,
141+ `export LOGNAME=${ shellEscape ( sshUser ) } ` ,
142+ `export CODEX_HOME=${ shellEscape ( codexHome ) } ` ,
143+ "export DOCKER_GIT_RTK_ENABLE=\"${DOCKER_GIT_RTK_ENABLE:-1}\"" ,
144+ "set -u" ,
101145 `cd ${ shellEscape ( cwd ) } ` ,
102146 exports ,
103- `exec ${ command } `
147+ execLine ( command )
104148 ]
105149 . filter ( ( line ) => line . trim ( ) . length > 0 )
106150 . join ( "\n" )
107151}
108152
153+ export const buildAgentDockerExecArgs = (
154+ project : Pick < ProjectDetails , "containerName" | "sshUser" | "codexHome" > ,
155+ script : string
156+ ) : ReadonlyArray < string > => {
157+ const home = agentHome ( project . sshUser )
158+
159+ return [
160+ "exec" ,
161+ "-i" ,
162+ "-u" ,
163+ project . sshUser ,
164+ "-e" ,
165+ `HOME=${ home } ` ,
166+ "-e" ,
167+ `USER=${ project . sshUser } ` ,
168+ "-e" ,
169+ `LOGNAME=${ project . sshUser } ` ,
170+ "-e" ,
171+ `CODEX_HOME=${ project . codexHome } ` ,
172+ project . containerName ,
173+ "bash" ,
174+ "-lc" ,
175+ script
176+ ]
177+ }
178+
109179const trimLogs = ( logs : Array < AgentLogLine > ) : Array < AgentLogLine > =>
110180 logs . length <= maxLogLines ? logs : logs . slice ( logs . length - maxLogLines )
111181
@@ -316,6 +386,14 @@ export const startAgent = (
316386 updatedAt : startedAt
317387 }
318388
389+ const script = buildAgentScript (
390+ sessionId ,
391+ workingDir ,
392+ project . sshUser ,
393+ project . codexHome ,
394+ request . env ?? [ ] ,
395+ command
396+ )
319397 const record : AgentRecord = {
320398 session,
321399 projectDir : project . projectDir ,
@@ -328,10 +406,9 @@ export const startAgent = (
328406 records . set ( sessionId , record )
329407 upsertProjectIndex ( project . id , sessionId )
330408
331- const script = buildAgentScript ( sessionId , workingDir , request . env ?? [ ] , command )
332409 const child = spawn (
333410 "docker" ,
334- [ "exec" , "-i" , project . containerName , "bash" , "-lc" , script ] ,
411+ [ ... buildAgentDockerExecArgs ( project , script ) ] ,
335412 {
336413 cwd : project . projectDir ,
337414 env : process . env ,
0 commit comments