Files
sandbox-runtime/test/config-validation.test.ts
Mohamad El Hajj 188f9a110d Merge main into elhajj/credential-deny-mode
Resolves conflicts in linux-sandbox-utils.ts (keep both
resolveSymlinkDenyDest and pushReadDenyDirMounts; skip symlink aliases
in the new file-mask re-emission loop so it never targets a symlink
bind dest) and sandbox-manager.ts (keep both the randomBytes import
and the CredentialsConfig type import).

Also suppresses await-thenable on linux-bridge-spawn-error.test.ts:27
where bun-types misdeclares .rejects matchers as void; lint-staged
flagged it during the merge commit (CI only lints src/).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 18:22:53 +00:00

516 lines
14 KiB
TypeScript

import { describe, test, expect } from 'bun:test'
import { SandboxRuntimeConfigSchema } from '../src/sandbox/sandbox-config.js'
describe('Config Validation', () => {
test('should validate a valid minimal config', () => {
const config = {
network: {
allowedDomains: [],
deniedDomains: [],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
})
test('should validate a config with valid domains', () => {
const config = {
network: {
allowedDomains: ['example.com', '*.github.com', 'localhost'],
deniedDomains: ['evil.com'],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
})
test('should reject invalid domain patterns', () => {
const config = {
network: {
allowedDomains: ['not-a-domain'],
deniedDomains: [],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(false)
})
test('should reject domain with protocol', () => {
const config = {
network: {
allowedDomains: ['https://example.com'],
deniedDomains: [],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(false)
})
test('should reject empty filesystem paths', () => {
const config = {
network: {
allowedDomains: [],
deniedDomains: [],
},
filesystem: {
denyRead: [''],
allowWrite: [],
denyWrite: [],
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(false)
})
test('should validate config with optional fields', () => {
const config = {
network: {
allowedDomains: ['example.com'],
deniedDomains: [],
allowUnixSockets: ['/var/run/docker.sock'],
allowAllUnixSockets: false,
allowLocalBinding: true,
},
filesystem: {
denyRead: ['/etc/shadow'],
allowWrite: ['/tmp'],
denyWrite: ['/etc'],
},
ignoreViolations: {
'*': ['/usr/bin'],
'git push': ['/usr/bin/nc'],
},
enableWeakerNestedSandbox: true,
enableWeakerNetworkIsolation: false,
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
})
test('should reject missing required fields', () => {
const config = {
network: {
allowedDomains: [],
},
filesystem: {
denyRead: [],
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(false)
})
test('should validate wildcard domains correctly', () => {
const validWildcards = ['*.example.com', '*.github.io', '*.co.uk']
const invalidWildcards = [
'*example.com', // Missing dot after asterisk
'*.com', // No subdomain
'*.', // Invalid format
]
for (const domain of validWildcards) {
const config = {
network: { allowedDomains: [domain], deniedDomains: [] },
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
}
for (const domain of invalidWildcards) {
const config = {
network: { allowedDomains: [domain], deniedDomains: [] },
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(false)
}
})
test('should validate config with enableWeakerNetworkIsolation', () => {
const config = {
network: {
allowedDomains: ['example.com'],
deniedDomains: [],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
enableWeakerNetworkIsolation: true,
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.enableWeakerNetworkIsolation).toBe(true)
}
})
test('should validate config with allowAppleEvents', () => {
const config = {
network: {
allowedDomains: ['example.com'],
deniedDomains: [],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
allowAppleEvents: true,
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.allowAppleEvents).toBe(true)
}
})
test('should validate config with custom ripgrep command', () => {
const config = {
network: {
allowedDomains: [],
deniedDomains: [],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
ripgrep: {
command: '/usr/local/bin/rg',
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.ripgrep?.command).toBe('/usr/local/bin/rg')
}
})
test('should validate config with custom ripgrep command and args', () => {
const config = {
network: {
allowedDomains: [],
deniedDomains: [],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
ripgrep: {
command: 'claude',
args: ['--ripgrep'],
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.ripgrep?.command).toBe('claude')
expect(result.data.ripgrep?.args).toEqual(['--ripgrep'])
}
})
test('should accept valid allowMachLookup entries', () => {
const config = {
network: {
allowedDomains: [],
deniedDomains: [],
allowMachLookup: [
'2BUA8C4S2C.com.1password.*',
'com.apple.CoreSimulator.CoreSimulatorService',
'*',
],
},
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
})
test.each(['com.*.foo', 'com.example.**'])(
'should reject allowMachLookup entry with non-trailing wildcard: %s',
entry => {
const config = {
network: {
allowedDomains: [],
deniedDomains: [],
allowMachLookup: [entry],
},
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(false)
},
)
test('should use default ripgrep command when not specified', () => {
const config = {
network: {
allowedDomains: [],
deniedDomains: [],
},
filesystem: {
denyRead: [],
allowWrite: [],
denyWrite: [],
},
}
const result = SandboxRuntimeConfigSchema.safeParse(config)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.ripgrep).toBeUndefined()
}
})
describe('bwrapPath / socatPath', () => {
const base = {
network: { allowedDomains: [], deniedDomains: [] },
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
}
test('accepts absolute paths', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
bwrapPath: '/usr/local/bin/bwrap',
socatPath: '/opt/tools/socat',
})
expect(result.success).toBe(true)
})
test('rejects relative bwrapPath', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
bwrapPath: 'bwrap',
})
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.issues[0]?.message).toContain('must be absolute')
}
})
test('rejects relative socatPath', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
socatPath: './bin/socat',
})
expect(result.success).toBe(false)
})
test('rejects empty bwrapPath', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
bwrapPath: '',
})
expect(result.success).toBe(false)
})
})
describe('credentials', () => {
const base = {
network: { allowedDomains: [], deniedDomains: [] },
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
}
test('is optional — config without it validates and stays undefined', () => {
const result = SandboxRuntimeConfigSchema.safeParse(base)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.credentials).toBeUndefined()
}
})
test('accepts file and env var entries with deny mode', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {
files: [
{ path: '~/.netrc', mode: 'deny' },
{ path: '~/.aws', mode: 'deny' },
],
envVars: [
{ name: 'AWS_SECRET_ACCESS_KEY', mode: 'deny' },
{ name: 'GH_TOKEN', mode: 'deny' },
],
},
})
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.credentials?.files).toHaveLength(2)
expect(result.data.credentials?.envVars).toHaveLength(2)
}
})
test('rejects mode "allow" — no longer a valid mode', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {
files: [{ path: '~/.aws', mode: 'allow' }],
},
})
expect(result.success).toBe(false)
})
test('accepts an empty credentials object', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {},
})
expect(result.success).toBe(true)
})
test('rejects mode "mask" for files with an actionable message', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {
files: [{ path: '~/.config/gh/hosts.yml', mode: 'mask' }],
},
})
expect(result.success).toBe(false)
if (!result.success) {
const messages = result.error.issues.map(i => i.message).join('\n')
expect(messages).toContain('not supported yet')
expect(messages).toContain('"mask"')
}
})
test('rejects mode "mask" for env vars with an actionable message', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {
envVars: [{ name: 'GH_TOKEN', mode: 'mask' }],
},
})
expect(result.success).toBe(false)
if (!result.success) {
const messages = result.error.issues.map(i => i.message).join('\n')
expect(messages).toContain('not supported yet')
}
})
test('rejects unknown modes', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {
files: [{ path: '~/.netrc', mode: 'block' }],
},
})
expect(result.success).toBe(false)
})
test('rejects empty file paths', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {
files: [{ path: '', mode: 'deny' }],
},
})
expect(result.success).toBe(false)
})
test.each(['', 'FOO=BAR', 'FOO BAR', '--bind', '-u', '1FOO', 'FOO.BAR'])(
'rejects invalid env var name: %j',
name => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {
envVars: [{ name, mode: 'deny' }],
},
})
expect(result.success).toBe(false)
},
)
test.each(['_FOO', 'FOO_BAR2'])('accepts valid env var name: %j', name => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
credentials: {
envVars: [{ name, mode: 'deny' }],
},
})
expect(result.success).toBe(true)
})
})
describe('network.tlsTerminate', () => {
const base = {
network: { allowedDomains: [], deniedDomains: [] },
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
}
test('is optional — config without it validates', () => {
expect(SandboxRuntimeConfigSchema.safeParse(base).success).toBe(true)
})
test('accepts caCertPath + caKeyPath', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
network: {
...base.network,
tlsTerminate: { caCertPath: '/etc/ca.crt', caKeyPath: '/etc/ca.key' },
},
})
expect(result.success).toBe(true)
})
test('rejects when caKeyPath is missing', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
network: {
...base.network,
tlsTerminate: { caCertPath: '/etc/ca.crt' },
},
})
expect(result.success).toBe(false)
})
test('rejects empty caCertPath', () => {
const result = SandboxRuntimeConfigSchema.safeParse({
...base,
network: {
...base.network,
tlsTerminate: { caCertPath: '', caKeyPath: '/etc/ca.key' },
},
})
expect(result.success).toBe(false)
})
})
})