Skip to content

Commit e647549

Browse files
committed
feat: custom env vars
1 parent 75187ac commit e647549

File tree

8 files changed

+339
-5
lines changed

8 files changed

+339
-5
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@xmz-ai/sandbox-runtime",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"description": "Xmz Sandbox Runtime - A general-purpose tool for wrapping security boundaries around arbitrary processes",
55
"type": "module",
66
"main": "./dist/index.js",

src/sandbox/linux-sandbox-utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
normalizeCaseForComparison,
1414
DANGEROUS_FILES,
1515
getDangerousDirectories,
16+
RESERVED_ENV_VARS,
1617
} from './sandbox-utils.js'
1718
import type {
1819
FsReadRestrictionConfig,
@@ -51,6 +52,8 @@ export interface LinuxSandboxParams {
5152
mandatoryDenySearchDepth?: number
5253
/** Abort signal to cancel the ripgrep scan */
5354
abortSignal?: AbortSignal
55+
/** Custom environment variables to set in the sandbox */
56+
envVars?: Array<{ name: string; value: string }>
5457
}
5558

5659
/** Default max depth for searching dangerous files */
@@ -1013,6 +1016,28 @@ export async function wrapCommandWithSandboxLinux(
10131016
// If no sockets provided, network is completely blocked (--unshare-net without proxy)
10141017
}
10151018

1019+
// ========== CUSTOM ENVIRONMENT VARIABLES ==========
1020+
if (params.envVars && params.envVars.length > 0) {
1021+
for (const { name, value } of params.envVars) {
1022+
if (RESERVED_ENV_VARS.has(name.toUpperCase())) {
1023+
logForDebugging(
1024+
`[Sandbox Linux] Skipping reserved environment variable: ${name}`,
1025+
{ level: 'warn' },
1026+
)
1027+
continue
1028+
}
1029+
bwrapArgs.push('--setenv', name, value)
1030+
}
1031+
const addedCount = params.envVars.filter(
1032+
v => !RESERVED_ENV_VARS.has(v.name.toUpperCase()),
1033+
).length
1034+
if (addedCount > 0) {
1035+
logForDebugging(
1036+
`[Sandbox Linux] Added ${addedCount} custom environment variable(s)`,
1037+
)
1038+
}
1039+
}
1040+
10161041
// ========== FILESYSTEM RESTRICTIONS ==========
10171042
const fsArgs = await generateFilesystemArgs(
10181043
readConfig,

src/sandbox/macos-sandbox-utils.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
containsGlobChars,
1111
DANGEROUS_FILES,
1212
getDangerousDirectories,
13+
RESERVED_ENV_VARS,
1314
} from './sandbox-utils.js'
1415
import type {
1516
FsReadRestrictionConfig,
@@ -29,6 +30,8 @@ export interface MacOSSandboxParams {
2930
writeConfig: FsWriteRestrictionConfig | undefined
3031
ignoreViolations?: IgnoreViolationsConfig | undefined
3132
binShell?: string
33+
/** Custom environment variables to set in the sandbox */
34+
envVars?: Array<{ name: string; value: string }>
3235
}
3336

3437
/**
@@ -674,7 +677,33 @@ export function wrapCommandWithSandboxMacOS(
674677
})
675678

676679
// Generate proxy environment variables using shared utility
677-
const proxyEnv = `export ${generateProxyEnvVars(httpProxyPort, socksProxyPort).join(' ')} && `
680+
const proxyEnvVars = generateProxyEnvVars(httpProxyPort, socksProxyPort)
681+
682+
// Add custom environment variables (with reserved var filtering)
683+
const customEnvVars: string[] = []
684+
if (params.envVars && params.envVars.length > 0) {
685+
for (const { name, value } of params.envVars) {
686+
if (RESERVED_ENV_VARS.has(name.toUpperCase())) {
687+
logForDebugging(
688+
`[Sandbox macOS] Skipping reserved environment variable: ${name}`,
689+
{ level: 'warn' },
690+
)
691+
continue
692+
}
693+
// Shell-escape the value for safety
694+
const escapedValue = value.replace(/'/g, "'\\''")
695+
customEnvVars.push(`${name}='${escapedValue}'`)
696+
}
697+
if (customEnvVars.length > 0) {
698+
logForDebugging(
699+
`[Sandbox macOS] Added ${customEnvVars.length} custom environment variable(s)`,
700+
)
701+
}
702+
}
703+
704+
const allEnvVars = [...proxyEnvVars, ...customEnvVars]
705+
const proxyEnv =
706+
allEnvVars.length > 0 ? `export ${allEnvVars.join(' ')} && ` : ''
678707

679708
// Use the user's shell (zsh, bash, etc.) to ensure aliases/snapshots work
680709
// Resolve the full path to the shell binary

src/sandbox/sandbox-config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,19 @@ export const RipgrepConfigSchema = z.object({
165165
),
166166
})
167167

168+
/**
169+
* Environment variables configuration schema
170+
* - String values: set the environment variable to that value
171+
* - null values: inherit from host environment (process.env)
172+
*/
173+
export const EnvConfigSchema = z
174+
.record(z.string(), z.string().nullable())
175+
.optional()
176+
.describe(
177+
'Custom environment variables to set in sandboxed processes. ' +
178+
'Keys are variable names, values can be strings (explicit value) or null (inherit from host).',
179+
)
180+
168181
/**
169182
* Main configuration schema for Sandbox Runtime validation
170183
*/
@@ -193,6 +206,9 @@ export const SandboxRuntimeConfigSchema = z.object({
193206
'Maximum directory depth to search for dangerous files on Linux (default: 3). ' +
194207
'Higher values provide more protection but slower performance.',
195208
),
209+
env: EnvConfigSchema.describe(
210+
'Custom environment variables for sandboxed processes',
211+
),
196212
})
197213

198214
// Export inferred types
@@ -202,4 +218,5 @@ export type IgnoreViolationsConfig = z.infer<
202218
typeof IgnoreViolationsConfigSchema
203219
>
204220
export type RipgrepConfig = z.infer<typeof RipgrepConfigSchema>
221+
export type EnvConfig = z.infer<typeof EnvConfigSchema>
205222
export type SandboxRuntimeConfig = z.infer<typeof SandboxRuntimeConfigSchema>

src/sandbox/sandbox-manager.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,35 @@ function getMandatoryDenySearchDepth(): number {
516516
return config?.mandatoryDenySearchDepth ?? 3
517517
}
518518

519+
/**
520+
* Get resolved environment variables configuration
521+
* Resolves inherited values from host environment
522+
* @returns Array of { name, value } pairs with all values resolved
523+
*/
524+
function getResolvedEnvVars(): Array<{ name: string; value: string }> {
525+
if (!config?.env) {
526+
return []
527+
}
528+
529+
const resolved: Array<{ name: string; value: string }> = []
530+
531+
for (const [name, configValue] of Object.entries(config.env)) {
532+
let value: string | undefined
533+
534+
if (configValue === null) {
535+
value = process.env[name]
536+
} else {
537+
value = configValue
538+
}
539+
540+
if (value !== undefined) {
541+
resolved.push({ name, value })
542+
}
543+
}
544+
545+
return resolved
546+
}
547+
519548
function getProxyPort(): number | undefined {
520549
return managerContext?.httpProxyPort
521550
}
@@ -607,6 +636,7 @@ async function wrapWithSandbox(
607636
allowLocalBinding: getAllowLocalBinding(),
608637
ignoreViolations: getIgnoreViolations(),
609638
binShell,
639+
envVars: getResolvedEnvVars(),
610640
})
611641

612642
case 'linux':
@@ -634,6 +664,7 @@ async function wrapWithSandbox(
634664
ripgrepConfig: getRipgrepConfig(),
635665
mandatoryDenySearchDepth: getMandatoryDenySearchDepth(),
636666
abortSignal,
667+
envVars: getResolvedEnvVars(),
637668
})
638669

639670
default:

src/sandbox/sandbox-utils.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@ export const DANGEROUS_FILES = [
2525
*/
2626
export const DANGEROUS_DIRECTORIES = ['.git', '.vscode', '.idea'] as const
2727

28+
/**
29+
* Environment variables reserved by sandbox-runtime
30+
* These cannot be overridden by user configuration
31+
* Note: Matching is case-insensitive (names are uppercased before checking)
32+
*/
33+
export const RESERVED_ENV_VARS = new Set([
34+
'HTTP_PROXY',
35+
'HTTPS_PROXY',
36+
'ALL_PROXY',
37+
'NO_PROXY',
38+
'FTP_PROXY',
39+
'GRPC_PROXY',
40+
'RSYNC_PROXY',
41+
'GIT_SSH_COMMAND',
42+
'DOCKER_HTTP_PROXY',
43+
'DOCKER_HTTPS_PROXY',
44+
'CLOUDSDK_PROXY_TYPE',
45+
'CLOUDSDK_PROXY_ADDRESS',
46+
'CLOUDSDK_PROXY_PORT',
47+
'CLAUDE_CODE_HOST_HTTP_PROXY_PORT',
48+
'CLAUDE_CODE_HOST_SOCKS_PROXY_PORT',
49+
'SANDBOX_RUNTIME',
50+
'TMPDIR',
51+
])
52+
2853
/**
2954
* Get the list of dangerous directories to deny writes to.
3055
* Excludes .git since we need it writable for git operations -
@@ -144,7 +169,7 @@ export function generateProxyEnvVars(
144169
httpProxyPort?: number,
145170
socksProxyPort?: number,
146171
): string[] {
147-
const envVars: string[] = [`SANDBOX_RUNTIME=1`, `TMPDIR=/tmp/claude`]
172+
const envVars: string[] = [`SANDBOX_RUNTIME=1`, `TMPDIR=/tmp/xmz-ai-sandbox`]
148173

149174
// If no proxy ports provided, return minimal env vars
150175
if (!httpProxyPort && !socksProxyPort) {

0 commit comments

Comments
 (0)