@@ -108,6 +108,38 @@ public actor SandboxService {
108108 return URL ( fileURLWithPath: resolved. path)
109109 }
110110
111+ /// Merges start-time environment overrides into a base environment list.
112+ /// Existing keys are replaced in-place and new keys are appended.
113+ /// Made static for unit testability.
114+ public static func mergedEnvironmentVariables( base: [ String ] , overrides: [ String : String ] ) -> [ String ] {
115+ guard !overrides. isEmpty else {
116+ return base
117+ }
118+
119+ var env = base
120+ var pending = overrides
121+
122+ for (index, entry) in env. enumerated ( ) {
123+ guard let separator = entry. firstIndex ( of: " = " ) else {
124+ continue
125+ }
126+
127+ let key = String ( entry [ ..< separator] )
128+ if let overrideValue = pending. removeValue ( forKey: key) {
129+ env [ index] = " \( key) = \( overrideValue) "
130+ }
131+ }
132+
133+ for key in pending. keys. sorted ( ) {
134+ guard let value = pending [ key] else {
135+ continue
136+ }
137+ env. append ( " \( key) = \( value) " )
138+ }
139+
140+ return env
141+ }
142+
111143 /// Create an instance with a bundle that describes the container.
112144 ///
113145 /// - Parameters:
@@ -177,10 +209,15 @@ public actor SandboxService {
177209 try bundle. createLogFile ( )
178210
179211 var config = try bundle. configuration
180- // Extract sshAuthSocketPath from the XPC request; if present and config.ssh is true,
181- // we mount the socket and set SSH_AUTH_SOCK in the process environment (replacing the
182- // previous behavior that used the sandbox's launch env).
183- let bootstrapSshAuthPath = message. string ( key: SandboxKeys . sshAuthSocketPath. rawValue)
212+ // Extract dynamic env overrides from the XPC request; when SSH forwarding is enabled,
213+ // SSH_AUTH_SOCK from this map provides the host socket path to mount.
214+ let dynamicEnv : [ String : String ] =
215+ if let data = message. dataNoCopy ( key: SandboxKeys . dynamicEnv. rawValue) {
216+ try JSONDecoder ( ) . decode ( [ String : String ] . self, from: data)
217+ } else {
218+ [ : ]
219+ }
220+ let bootstrapSshAuthPath = dynamicEnv [ Self . sshAuthSocketEnvVar]
184221
185222 if config. ssh {
186223 if let resolved = Self . resolveSSHAuthSocketHostPath ( config: config, bootstrapOverridePath: bootstrapSshAuthPath) {
@@ -273,7 +310,12 @@ public actor SandboxService {
273310 let id = config. id
274311 let rootfs = try bundle. containerRootfs. asMount
275312 let container = try LinuxContainer ( id, rootfs: rootfs, vmm: vmm, logger: self . log) { czConfig in
276- try Self . configureContainer ( czConfig: & czConfig, config: config, bootstrapOverridePath: bootstrapSshAuthPath)
313+ try Self . configureContainer (
314+ czConfig: & czConfig,
315+ config: config,
316+ bootstrapOverridePath: bootstrapSshAuthPath,
317+ dynamicEnv: dynamicEnv
318+ )
277319 czConfig. interfaces = interfaces
278320 czConfig. process. stdout = stdout
279321 czConfig. process. stderr = stderr
@@ -899,7 +941,8 @@ public actor SandboxService {
899941 private static func configureContainer(
900942 czConfig: inout LinuxContainer. Configuration,
901943 config: ContainerConfiguration,
902- bootstrapOverridePath: String? = nil
944+ bootstrapOverridePath: String? = nil ,
945+ dynamicEnv: [ String: String] = [ : ]
903946 ) throws {
904947 czConfig. cpus = config. resources. cpus
905948 czConfig. memoryInBytes = config. resources. memoryInBytes
@@ -958,7 +1001,12 @@ public actor SandboxService {
9581001 searchDomains: dns. searchDomains, options: dns. options)
9591002 }
9601003
961- try Self . configureInitialProcess ( czConfig: & czConfig, config: config, bootstrapOverridePath: bootstrapOverridePath)
1004+ try Self . configureInitialProcess (
1005+ czConfig: & czConfig,
1006+ config: config,
1007+ bootstrapOverridePath: bootstrapOverridePath,
1008+ dynamicEnv: dynamicEnv
1009+ )
9621010 }
9631011
9641012 private func getDefaultNameservers( allocatedAttachments: [ AllocatedAttachment] ) async throws -> [ String] {
@@ -976,17 +1024,20 @@ public actor SandboxService {
9761024 private static func configureInitialProcess(
9771025 czConfig: inout LinuxContainer. Configuration,
9781026 config: ContainerConfiguration,
979- bootstrapOverridePath: String? = nil
1027+ bootstrapOverridePath: String? = nil ,
1028+ dynamicEnv: [ String: String] = [ : ]
9801029 ) throws {
9811030 let process = config. initProcess
9821031
9831032 czConfig. process. arguments = [ process. executable] + process. arguments
984- czConfig. process. environmentVariables = process. environment
1033+ czConfig. process. environmentVariables = Self . mergedEnvironmentVariables (
1034+ base: process. environment,
1035+ overrides: dynamicEnv
1036+ )
9851037
9861038 if Self . sshAuthSocketHostUrl ( config: config, bootstrapOverridePath: bootstrapOverridePath) != nil {
987- if !czConfig. process. environmentVariables. contains ( where: { $0. starts ( with: " \( Self . sshAuthSocketEnvVar) = " ) } ) {
988- czConfig. process. environmentVariables. append ( " \( Self . sshAuthSocketEnvVar) = \( Self . sshAuthSocketGuestPath) " )
989- }
1039+ czConfig. process. environmentVariables. removeAll ( where: { $0. starts ( with: " \( Self . sshAuthSocketEnvVar) = " ) } )
1040+ czConfig. process. environmentVariables. append ( " \( Self . sshAuthSocketEnvVar) = \( Self . sshAuthSocketGuestPath) " )
9901041 }
9911042
9921043 czConfig. process. terminal = process. terminal
0 commit comments