feat(core): implement SandboxManager interface and config schema

- Add `sandbox` block to `ConfigSchema` with `enabled`, `allowedPaths`,
  and `networkAccess` properties.
- Define the `SandboxManager` interface and request/response types.
- Implement `NoopSandboxManager` fallback that silently passes commands
  through but rigorously enforces environment variable sanitization via
  `sanitizeEnvironment`.
- Update config and sandbox tests to use the new `SandboxConfig` schema.
- Add `createMockSandboxConfig` utility to `test-utils` for cleaner test
  mocking across the monorepo.
This commit is contained in:
galz10
2026-03-09 11:20:13 -07:00
parent 09e99824d4
commit 863a0aa01e
11 changed files with 494 additions and 65 deletions
@@ -0,0 +1,111 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { NoopSandboxManager } from './sandboxManager.js';
describe('NoopSandboxManager', () => {
const sandboxManager = new NoopSandboxManager();
it('should pass through the command and arguments unchanged', async () => {
const req = {
command: 'ls',
args: ['-la'],
cwd: '/tmp',
env: { PATH: '/usr/bin' },
};
const result = await sandboxManager.prepareCommand(req);
expect(result.program).toBe('ls');
expect(result.args).toEqual(['-la']);
});
it('should sanitize the environment variables', async () => {
const req = {
command: 'echo',
args: ['hello'],
cwd: '/tmp',
env: {
PATH: '/usr/bin',
GITHUB_TOKEN: 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
MY_SECRET: 'super-secret',
SAFE_VAR: 'is-safe',
},
};
const result = await sandboxManager.prepareCommand(req);
expect(result.env['PATH']).toBe('/usr/bin');
expect(result.env['SAFE_VAR']).toBe('is-safe');
expect(result.env['GITHUB_TOKEN']).toBeUndefined();
expect(result.env['MY_SECRET']).toBeUndefined();
});
it('should force environment variable redaction even if not requested in config', async () => {
const req = {
command: 'echo',
args: ['hello'],
cwd: '/tmp',
env: {
API_KEY: 'sensitive-key',
},
config: {
sanitizationConfig: {
enableEnvironmentVariableRedaction: false,
},
},
};
const result = await sandboxManager.prepareCommand(req);
expect(result.env['API_KEY']).toBeUndefined();
});
it('should respect allowedEnvironmentVariables in config', async () => {
const req = {
command: 'echo',
args: ['hello'],
cwd: '/tmp',
env: {
MY_TOKEN: 'secret-token',
OTHER_SECRET: 'another-secret',
},
config: {
sanitizationConfig: {
allowedEnvironmentVariables: ['MY_TOKEN'],
},
},
};
const result = await sandboxManager.prepareCommand(req);
expect(result.env['MY_TOKEN']).toBe('secret-token');
expect(result.env['OTHER_SECRET']).toBeUndefined();
});
it('should respect blockedEnvironmentVariables in config', async () => {
const req = {
command: 'echo',
args: ['hello'],
cwd: '/tmp',
env: {
SAFE_VAR: 'safe-value',
BLOCKED_VAR: 'blocked-value',
},
config: {
sanitizationConfig: {
blockedEnvironmentVariables: ['BLOCKED_VAR'],
},
},
};
const result = await sandboxManager.prepareCommand(req);
expect(result.env['SAFE_VAR']).toBe('safe-value');
expect(result.env['BLOCKED_VAR']).toBeUndefined();
});
});
@@ -0,0 +1,78 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
sanitizeEnvironment,
type EnvironmentSanitizationConfig,
} from './environmentSanitization.js';
/**
* Request for preparing a command to run in a sandbox.
*/
export interface SandboxRequest {
/** The program to execute. */
command: string;
/** Arguments for the program. */
args: string[];
/** The working directory. */
cwd: string;
/** Environment variables to be passed to the program. */
env: NodeJS.ProcessEnv;
/** Optional sandbox-specific configuration. */
config?: {
sanitizationConfig?: Partial<EnvironmentSanitizationConfig>;
};
}
/**
* A command that has been prepared for sandboxed execution.
*/
export interface SandboxedCommand {
/** The program or wrapper to execute. */
program: string;
/** Final arguments for the program. */
args: string[];
/** Sanitized environment variables. */
env: NodeJS.ProcessEnv;
}
/**
* Interface for a service that prepares commands for sandboxed execution.
*/
export interface SandboxManager {
/**
* Prepares a command to run in a sandbox, including environment sanitization.
*/
prepareCommand(req: SandboxRequest): Promise<SandboxedCommand>;
}
/**
* A no-op implementation of SandboxManager that silently passes commands
* through while applying environment sanitization.
*/
export class NoopSandboxManager implements SandboxManager {
/**
* Prepares a command by sanitizing the environment and passing through
* the original program and arguments.
*/
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
const sanitizationConfig: EnvironmentSanitizationConfig = {
allowedEnvironmentVariables:
req.config?.sanitizationConfig?.allowedEnvironmentVariables ?? [],
blockedEnvironmentVariables:
req.config?.sanitizationConfig?.blockedEnvironmentVariables ?? [],
enableEnvironmentVariableRedaction: true, // Forced for safety
};
const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
return {
program: req.command,
args: req.args,
env: sanitizedEnv,
};
}
}