mirror of
https://github.com/google-gemini/gemini-cli
synced 2026-04-21 13:37:17 +00:00
feat(core): implement native Windows sandboxing (#21807)
This commit is contained in:
parent
06a7873c51
commit
c9a336976b
23 changed files with 1365 additions and 149 deletions
1
.geminiignore
Normal file
1
.geminiignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
packages/core/src/services/scripts/*.exe
|
||||||
|
|
@ -50,7 +50,25 @@ Cross-platform sandboxing with complete process isolation.
|
||||||
**Note**: Requires building the sandbox image locally or using a published image
|
**Note**: Requires building the sandbox image locally or using a published image
|
||||||
from your organization's registry.
|
from your organization's registry.
|
||||||
|
|
||||||
### 3. gVisor / runsc (Linux only)
|
### 3. Windows Native Sandbox (Windows only)
|
||||||
|
|
||||||
|
... **Troubleshooting and Side Effects:**
|
||||||
|
|
||||||
|
The Windows Native sandbox uses the `icacls` command to set a "Low Mandatory
|
||||||
|
Level" on files and directories it needs to write to.
|
||||||
|
|
||||||
|
- **Persistence**: These integrity level changes are persistent on the
|
||||||
|
filesystem. Even after the sandbox session ends, files created or modified by
|
||||||
|
the sandbox will retain their "Low" integrity level.
|
||||||
|
- **Manual Reset**: If you need to reset the integrity level of a file or
|
||||||
|
directory, you can use:
|
||||||
|
```powershell
|
||||||
|
icacls "C:\path\to\dir" /setintegritylevel Medium
|
||||||
|
```
|
||||||
|
- **System Folders**: The sandbox manager automatically skips setting integrity
|
||||||
|
levels on system folders (like `C:\Windows`) for safety.
|
||||||
|
|
||||||
|
### 4. gVisor / runsc (Linux only)
|
||||||
|
|
||||||
Strongest isolation available: runs containers inside a user-space kernel via
|
Strongest isolation available: runs containers inside a user-space kernel via
|
||||||
[gVisor](https://github.com/google/gvisor). gVisor intercepts all container
|
[gVisor](https://github.com/google/gvisor). gVisor intercepts all container
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ they appear in the UI.
|
||||||
|
|
||||||
| UI Label | Setting | Description | Default |
|
| UI Label | Setting | Description | Default |
|
||||||
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
| -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||||
|
| Sandbox Allowed Paths | `tools.sandboxAllowedPaths` | List of additional paths that the sandbox is allowed to access. | `[]` |
|
||||||
|
| Sandbox Network Access | `tools.sandboxNetworkAccess` | Whether the sandbox is allowed to access the network. | `false` |
|
||||||
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
|
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
|
||||||
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
|
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
|
||||||
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
|
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
|
||||||
|
|
|
||||||
|
|
@ -1276,10 +1276,21 @@ their corresponding top-level category object in your `settings.json` file.
|
||||||
- **Description:** Legacy full-process sandbox execution environment. Set to a
|
- **Description:** Legacy full-process sandbox execution environment. Set to a
|
||||||
boolean to enable or disable the sandbox, provide a string path to a sandbox
|
boolean to enable or disable the sandbox, provide a string path to a sandbox
|
||||||
profile, or specify an explicit sandbox command (e.g., "docker", "podman",
|
profile, or specify an explicit sandbox command (e.g., "docker", "podman",
|
||||||
"lxc").
|
"lxc", "windows-native").
|
||||||
- **Default:** `undefined`
|
- **Default:** `undefined`
|
||||||
- **Requires restart:** Yes
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`tools.sandboxAllowedPaths`** (array):
|
||||||
|
- **Description:** List of additional paths that the sandbox is allowed to
|
||||||
|
access.
|
||||||
|
- **Default:** `[]`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`tools.sandboxNetworkAccess`** (boolean):
|
||||||
|
- **Description:** Whether the sandbox is allowed to access the network.
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
- **`tools.shell.enableInteractiveShell`** (boolean):
|
- **`tools.shell.enableInteractiveShell`** (boolean):
|
||||||
- **Description:** Use node-pty for an interactive shell experience. Fallback
|
- **Description:** Use node-pty for an interactive shell experience. Fallback
|
||||||
to child_process still applies.
|
to child_process still applies.
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,12 @@ export default tseslint.config(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/core/scripts/**/*.{js,mjs}'],
|
files: [
|
||||||
|
'./scripts/**/*.js',
|
||||||
|
'packages/*/scripts/**/*.js',
|
||||||
|
'esbuild.config.js',
|
||||||
|
'packages/core/scripts/**/*.{js,mjs}',
|
||||||
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
...globals.node,
|
...globals.node,
|
||||||
|
|
|
||||||
|
|
@ -702,6 +702,19 @@ export async function loadCliConfig(
|
||||||
? defaultModel
|
? defaultModel
|
||||||
: specifiedModel || defaultModel;
|
: specifiedModel || defaultModel;
|
||||||
const sandboxConfig = await loadSandboxConfig(settings, argv);
|
const sandboxConfig = await loadSandboxConfig(settings, argv);
|
||||||
|
if (sandboxConfig) {
|
||||||
|
const existingPaths = sandboxConfig.allowedPaths || [];
|
||||||
|
if (settings.tools.sandboxAllowedPaths?.length) {
|
||||||
|
sandboxConfig.allowedPaths = [
|
||||||
|
...new Set([...existingPaths, ...settings.tools.sandboxAllowedPaths]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (settings.tools.sandboxNetworkAccess !== undefined) {
|
||||||
|
sandboxConfig.networkAccess =
|
||||||
|
sandboxConfig.networkAccess || settings.tools.sandboxNetworkAccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const screenReader =
|
const screenReader =
|
||||||
argv.screenReader !== undefined
|
argv.screenReader !== undefined
|
||||||
? argv.screenReader
|
? argv.screenReader
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,8 @@ describe('loadSandboxConfig', () => {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
command: 'podman',
|
command: 'podman',
|
||||||
|
allowedPaths: [],
|
||||||
|
networkAccess: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -353,6 +355,8 @@ describe('loadSandboxConfig', () => {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
image: 'custom/image',
|
image: 'custom/image',
|
||||||
|
allowedPaths: [],
|
||||||
|
networkAccess: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -367,6 +371,8 @@ describe('loadSandboxConfig', () => {
|
||||||
tools: {
|
tools: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
allowedPaths: [],
|
||||||
|
networkAccess: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -382,6 +388,7 @@ describe('loadSandboxConfig', () => {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowedPaths: ['/settings-path'],
|
allowedPaths: ['/settings-path'],
|
||||||
|
networkAccess: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ const VALID_SANDBOX_COMMANDS = [
|
||||||
'sandbox-exec',
|
'sandbox-exec',
|
||||||
'runsc',
|
'runsc',
|
||||||
'lxc',
|
'lxc',
|
||||||
|
'windows-native',
|
||||||
];
|
];
|
||||||
|
|
||||||
function isSandboxCommand(
|
function isSandboxCommand(
|
||||||
|
|
@ -75,8 +76,15 @@ function getSandboxCommand(
|
||||||
'gVisor (runsc) sandboxing is only supported on Linux',
|
'gVisor (runsc) sandboxing is only supported on Linux',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// confirm that specified command exists
|
// windows-native is only supported on Windows
|
||||||
if (!commandExists.sync(sandbox)) {
|
if (sandbox === 'windows-native' && os.platform() !== 'win32') {
|
||||||
|
throw new FatalSandboxError(
|
||||||
|
'Windows native sandboxing is only supported on Windows',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirm that specified command exists (unless it's built-in)
|
||||||
|
if (sandbox !== 'windows-native' && !commandExists.sync(sandbox)) {
|
||||||
throw new FatalSandboxError(
|
throw new FatalSandboxError(
|
||||||
`Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
|
`Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
|
||||||
);
|
);
|
||||||
|
|
@ -149,7 +157,12 @@ export async function loadSandboxConfig(
|
||||||
customImage ??
|
customImage ??
|
||||||
packageJson?.config?.sandboxImageUri;
|
packageJson?.config?.sandboxImageUri;
|
||||||
|
|
||||||
return command && image
|
const isNative =
|
||||||
|
command === 'windows-native' ||
|
||||||
|
command === 'sandbox-exec' ||
|
||||||
|
command === 'lxc';
|
||||||
|
|
||||||
|
return command && (image || isNative)
|
||||||
? { enabled: true, allowedPaths, networkAccess, command, image }
|
? { enabled: true, allowedPaths, networkAccess, command, image }
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1358,10 +1358,30 @@ const SETTINGS_SCHEMA = {
|
||||||
description: oneLine`
|
description: oneLine`
|
||||||
Legacy full-process sandbox execution environment.
|
Legacy full-process sandbox execution environment.
|
||||||
Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile,
|
Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile,
|
||||||
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc").
|
or specify an explicit sandbox command (e.g., "docker", "podman", "lxc", "windows-native").
|
||||||
`,
|
`,
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
|
sandboxAllowedPaths: {
|
||||||
|
type: 'array',
|
||||||
|
label: 'Sandbox Allowed Paths',
|
||||||
|
category: 'Tools',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: [] as string[],
|
||||||
|
description:
|
||||||
|
'List of additional paths that the sandbox is allowed to access.',
|
||||||
|
showInDialog: true,
|
||||||
|
items: { type: 'string' },
|
||||||
|
},
|
||||||
|
sandboxNetworkAccess: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Sandbox Network Access',
|
||||||
|
category: 'Tools',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: false,
|
||||||
|
description: 'Whether the sandbox is allowed to access the network.',
|
||||||
|
showInDialog: true,
|
||||||
|
},
|
||||||
shell: {
|
shell: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
label: 'Shell',
|
label: 'Shell',
|
||||||
|
|
|
||||||
121
packages/core/scripts/compile-windows-sandbox.js
Normal file
121
packages/core/scripts/compile-windows-sandbox.js
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles the GeminiSandbox C# helper on Windows.
|
||||||
|
* This is used to provide native restricted token sandboxing.
|
||||||
|
*/
|
||||||
|
function compileWindowsSandbox() {
|
||||||
|
if (os.platform() !== 'win32') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcHelperPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../src/services/scripts/GeminiSandbox.exe',
|
||||||
|
);
|
||||||
|
const distHelperPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../dist/src/services/scripts/GeminiSandbox.exe',
|
||||||
|
);
|
||||||
|
const sourcePath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../src/services/scripts/GeminiSandbox.cs',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
console.error(`Sandbox source not found at ${sourcePath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directories exist
|
||||||
|
[srcHelperPath, distHelperPath].forEach((p) => {
|
||||||
|
const dir = path.dirname(p);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find csc.exe (C# Compiler) which is built into Windows .NET Framework
|
||||||
|
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
|
||||||
|
const cscPaths = [
|
||||||
|
'csc.exe', // Try in PATH first
|
||||||
|
path.join(
|
||||||
|
systemRoot,
|
||||||
|
'Microsoft.NET',
|
||||||
|
'Framework64',
|
||||||
|
'v4.0.30319',
|
||||||
|
'csc.exe',
|
||||||
|
),
|
||||||
|
path.join(
|
||||||
|
systemRoot,
|
||||||
|
'Microsoft.NET',
|
||||||
|
'Framework',
|
||||||
|
'v4.0.30319',
|
||||||
|
'csc.exe',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let csc = undefined;
|
||||||
|
for (const p of cscPaths) {
|
||||||
|
if (p === 'csc.exe') {
|
||||||
|
const result = spawnSync('where', ['csc.exe'], { stdio: 'ignore' });
|
||||||
|
if (result.status === 0) {
|
||||||
|
csc = 'csc.exe';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (fs.existsSync(p)) {
|
||||||
|
csc = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!csc) {
|
||||||
|
console.warn(
|
||||||
|
'Windows C# compiler (csc.exe) not found. Native sandboxing will attempt to compile on first run.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Compiling native Windows sandbox helper...`);
|
||||||
|
// Compile to src
|
||||||
|
let result = spawnSync(
|
||||||
|
csc,
|
||||||
|
[`/out:${srcHelperPath}`, '/optimize', sourcePath],
|
||||||
|
{
|
||||||
|
stdio: 'inherit',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status === 0) {
|
||||||
|
console.log('Successfully compiled GeminiSandbox.exe to src');
|
||||||
|
// Copy to dist if dist exists
|
||||||
|
const distDir = path.resolve(__dirname, '../dist');
|
||||||
|
if (fs.existsSync(distDir)) {
|
||||||
|
const distScriptsDir = path.dirname(distHelperPath);
|
||||||
|
if (!fs.existsSync(distScriptsDir)) {
|
||||||
|
fs.mkdirSync(distScriptsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.copyFileSync(srcHelperPath, distHelperPath);
|
||||||
|
console.log('Successfully copied GeminiSandbox.exe to dist');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Failed to compile Windows sandbox helper.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileWindowsSandbox();
|
||||||
|
|
@ -42,9 +42,11 @@ import type { HookDefinition, HookEventName } from '../hooks/types.js';
|
||||||
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
||||||
import { GitService } from '../services/gitService.js';
|
import { GitService } from '../services/gitService.js';
|
||||||
import {
|
import {
|
||||||
createSandboxManager,
|
|
||||||
type SandboxManager,
|
type SandboxManager,
|
||||||
|
NoopSandboxManager,
|
||||||
} from '../services/sandboxManager.js';
|
} from '../services/sandboxManager.js';
|
||||||
|
import { createSandboxManager } from '../services/sandboxManagerFactory.js';
|
||||||
|
import { SandboxedFileSystemService } from '../services/sandboxedFileSystemService.js';
|
||||||
import {
|
import {
|
||||||
initializeTelemetry,
|
initializeTelemetry,
|
||||||
DEFAULT_TELEMETRY_TARGET,
|
DEFAULT_TELEMETRY_TARGET,
|
||||||
|
|
@ -467,7 +469,13 @@ export interface SandboxConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
allowedPaths?: string[];
|
allowedPaths?: string[];
|
||||||
networkAccess?: boolean;
|
networkAccess?: boolean;
|
||||||
command?: 'docker' | 'podman' | 'sandbox-exec' | 'runsc' | 'lxc';
|
command?:
|
||||||
|
| 'docker'
|
||||||
|
| 'podman'
|
||||||
|
| 'sandbox-exec'
|
||||||
|
| 'runsc'
|
||||||
|
| 'lxc'
|
||||||
|
| 'windows-native';
|
||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -478,7 +486,14 @@ export const ConfigSchema = z.object({
|
||||||
allowedPaths: z.array(z.string()).default([]),
|
allowedPaths: z.array(z.string()).default([]),
|
||||||
networkAccess: z.boolean().default(false),
|
networkAccess: z.boolean().default(false),
|
||||||
command: z
|
command: z
|
||||||
.enum(['docker', 'podman', 'sandbox-exec', 'runsc', 'lxc'])
|
.enum([
|
||||||
|
'docker',
|
||||||
|
'podman',
|
||||||
|
'sandbox-exec',
|
||||||
|
'runsc',
|
||||||
|
'lxc',
|
||||||
|
'windows-native',
|
||||||
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
@ -876,7 +891,6 @@ export class Config implements McpContext, AgentLoopContext {
|
||||||
this.approvedPlanPath = undefined;
|
this.approvedPlanPath = undefined;
|
||||||
this.embeddingModel =
|
this.embeddingModel =
|
||||||
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
|
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
|
||||||
this.fileSystemService = new StandardFileSystemService();
|
|
||||||
this.sandbox = params.sandbox
|
this.sandbox = params.sandbox
|
||||||
? {
|
? {
|
||||||
enabled: params.sandbox.enabled ?? false,
|
enabled: params.sandbox.enabled ?? false,
|
||||||
|
|
@ -890,6 +904,21 @@ export class Config implements McpContext, AgentLoopContext {
|
||||||
allowedPaths: [],
|
allowedPaths: [],
|
||||||
networkAccess: false,
|
networkAccess: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._sandboxManager = createSandboxManager(this.sandbox, params.targetDir);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(this._sandboxManager instanceof NoopSandboxManager) &&
|
||||||
|
this.sandbox.enabled
|
||||||
|
) {
|
||||||
|
this.fileSystemService = new SandboxedFileSystemService(
|
||||||
|
this._sandboxManager,
|
||||||
|
params.targetDir,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.fileSystemService = new StandardFileSystemService();
|
||||||
|
}
|
||||||
|
|
||||||
this.targetDir = path.resolve(params.targetDir);
|
this.targetDir = path.resolve(params.targetDir);
|
||||||
this.folderTrust = params.folderTrust ?? false;
|
this.folderTrust = params.folderTrust ?? false;
|
||||||
this.workspaceContext = new WorkspaceContext(this.targetDir, []);
|
this.workspaceContext = new WorkspaceContext(this.targetDir, []);
|
||||||
|
|
@ -1072,7 +1101,8 @@ export class Config implements McpContext, AgentLoopContext {
|
||||||
showColor: params.shellExecutionConfig?.showColor ?? false,
|
showColor: params.shellExecutionConfig?.showColor ?? false,
|
||||||
pager: params.shellExecutionConfig?.pager ?? 'cat',
|
pager: params.shellExecutionConfig?.pager ?? 'cat',
|
||||||
sanitizationConfig: this.sanitizationConfig,
|
sanitizationConfig: this.sanitizationConfig,
|
||||||
sandboxManager: this.sandboxManager,
|
sandboxManager: this._sandboxManager,
|
||||||
|
sandboxConfig: this.sandbox,
|
||||||
};
|
};
|
||||||
this.truncateToolOutputThreshold =
|
this.truncateToolOutputThreshold =
|
||||||
params.truncateToolOutputThreshold ??
|
params.truncateToolOutputThreshold ??
|
||||||
|
|
@ -1194,12 +1224,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._geminiClient = new GeminiClient(this);
|
this._geminiClient = new GeminiClient(this);
|
||||||
this._sandboxManager = createSandboxManager(
|
|
||||||
params.toolSandboxing ?? false,
|
|
||||||
this.targetDir,
|
|
||||||
);
|
|
||||||
this.a2aClientManager = new A2AClientManager(this);
|
this.a2aClientManager = new A2AClientManager(this);
|
||||||
this.shellExecutionConfig.sandboxManager = this._sandboxManager;
|
|
||||||
this.modelRouterService = new ModelRouterService(this);
|
this.modelRouterService = new ModelRouterService(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,8 @@ export * from './services/gitService.js';
|
||||||
export * from './services/FolderTrustDiscoveryService.js';
|
export * from './services/FolderTrustDiscoveryService.js';
|
||||||
export * from './services/chatRecordingService.js';
|
export * from './services/chatRecordingService.js';
|
||||||
export * from './services/fileSystemService.js';
|
export * from './services/fileSystemService.js';
|
||||||
|
export * from './services/sandboxedFileSystemService.js';
|
||||||
|
export * from './services/windowsSandboxManager.js';
|
||||||
export * from './services/sessionSummaryUtils.js';
|
export * from './services/sessionSummaryUtils.js';
|
||||||
export * from './services/contextManager.js';
|
export * from './services/contextManager.js';
|
||||||
export * from './services/trackerService.js';
|
export * from './services/trackerService.js';
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,11 @@
|
||||||
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import { NoopSandboxManager } from './sandboxManager.js';
|
||||||
NoopSandboxManager,
|
import { createSandboxManager } from './sandboxManagerFactory.js';
|
||||||
LocalSandboxManager,
|
|
||||||
createSandboxManager,
|
|
||||||
} from './sandboxManager.js';
|
|
||||||
import { LinuxSandboxManager } from '../sandbox/linux/LinuxSandboxManager.js';
|
import { LinuxSandboxManager } from '../sandbox/linux/LinuxSandboxManager.js';
|
||||||
import { MacOsSandboxManager } from '../sandbox/macos/MacOsSandboxManager.js';
|
import { MacOsSandboxManager } from '../sandbox/macos/MacOsSandboxManager.js';
|
||||||
|
import { WindowsSandboxManager } from './windowsSandboxManager.js';
|
||||||
|
|
||||||
describe('NoopSandboxManager', () => {
|
describe('NoopSandboxManager', () => {
|
||||||
const sandboxManager = new NoopSandboxManager();
|
const sandboxManager = new NoopSandboxManager();
|
||||||
|
|
@ -121,20 +119,20 @@ describe('NoopSandboxManager', () => {
|
||||||
|
|
||||||
describe('createSandboxManager', () => {
|
describe('createSandboxManager', () => {
|
||||||
it('should return NoopSandboxManager if sandboxing is disabled', () => {
|
it('should return NoopSandboxManager if sandboxing is disabled', () => {
|
||||||
const manager = createSandboxManager(false, '/workspace');
|
const manager = createSandboxManager({ enabled: false }, '/workspace');
|
||||||
expect(manager).toBeInstanceOf(NoopSandboxManager);
|
expect(manager).toBeInstanceOf(NoopSandboxManager);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ platform: 'linux', expected: LinuxSandboxManager },
|
{ platform: 'linux', expected: LinuxSandboxManager },
|
||||||
{ platform: 'darwin', expected: MacOsSandboxManager },
|
{ platform: 'darwin', expected: MacOsSandboxManager },
|
||||||
{ platform: 'win32', expected: LocalSandboxManager },
|
{ platform: 'win32', expected: WindowsSandboxManager },
|
||||||
] as const)(
|
] as const)(
|
||||||
'should return $expected.name if sandboxing is enabled and platform is $platform',
|
'should return $expected.name if sandboxing is enabled and platform is $platform',
|
||||||
({ platform, expected }) => {
|
({ platform, expected }) => {
|
||||||
const osSpy = vi.spyOn(os, 'platform').mockReturnValue(platform);
|
const osSpy = vi.spyOn(os, 'platform').mockReturnValue(platform);
|
||||||
try {
|
try {
|
||||||
const manager = createSandboxManager(true, '/workspace');
|
const manager = createSandboxManager({ enabled: true }, '/workspace');
|
||||||
expect(manager).toBeInstanceOf(expected);
|
expect(manager).toBeInstanceOf(expected);
|
||||||
} finally {
|
} finally {
|
||||||
osSpy.mockRestore();
|
osSpy.mockRestore();
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,11 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import os from 'node:os';
|
|
||||||
import {
|
import {
|
||||||
sanitizeEnvironment,
|
sanitizeEnvironment,
|
||||||
getSecureSanitizationConfig,
|
getSecureSanitizationConfig,
|
||||||
type EnvironmentSanitizationConfig,
|
type EnvironmentSanitizationConfig,
|
||||||
} from './environmentSanitization.js';
|
} from './environmentSanitization.js';
|
||||||
import { LinuxSandboxManager } from '../sandbox/linux/LinuxSandboxManager.js';
|
|
||||||
import { MacOsSandboxManager } from '../sandbox/macos/MacOsSandboxManager.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request for preparing a command to run in a sandbox.
|
* Request for preparing a command to run in a sandbox.
|
||||||
|
|
@ -28,6 +25,8 @@ export interface SandboxRequest {
|
||||||
/** Optional sandbox-specific configuration. */
|
/** Optional sandbox-specific configuration. */
|
||||||
config?: {
|
config?: {
|
||||||
sanitizationConfig?: Partial<EnvironmentSanitizationConfig>;
|
sanitizationConfig?: Partial<EnvironmentSanitizationConfig>;
|
||||||
|
allowedPaths?: string[];
|
||||||
|
networkAccess?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,21 +87,4 @@ export class LocalSandboxManager implements SandboxManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export { createSandboxManager } from './sandboxManagerFactory.js';
|
||||||
* Creates a sandbox manager based on the provided settings.
|
|
||||||
*/
|
|
||||||
export function createSandboxManager(
|
|
||||||
sandboxingEnabled: boolean,
|
|
||||||
workspace: string,
|
|
||||||
): SandboxManager {
|
|
||||||
if (sandboxingEnabled) {
|
|
||||||
if (os.platform() === 'linux') {
|
|
||||||
return new LinuxSandboxManager({ workspace });
|
|
||||||
}
|
|
||||||
if (os.platform() === 'darwin') {
|
|
||||||
return new MacOsSandboxManager({ workspace });
|
|
||||||
}
|
|
||||||
return new LocalSandboxManager();
|
|
||||||
}
|
|
||||||
return new NoopSandboxManager();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
45
packages/core/src/services/sandboxManagerFactory.ts
Normal file
45
packages/core/src/services/sandboxManagerFactory.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import os from 'node:os';
|
||||||
|
import {
|
||||||
|
type SandboxManager,
|
||||||
|
NoopSandboxManager,
|
||||||
|
LocalSandboxManager,
|
||||||
|
} from './sandboxManager.js';
|
||||||
|
import { LinuxSandboxManager } from '../sandbox/linux/LinuxSandboxManager.js';
|
||||||
|
import { MacOsSandboxManager } from '../sandbox/macos/MacOsSandboxManager.js';
|
||||||
|
import { WindowsSandboxManager } from './windowsSandboxManager.js';
|
||||||
|
import type { SandboxConfig } from '../config/config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a sandbox manager based on the provided settings.
|
||||||
|
*/
|
||||||
|
export function createSandboxManager(
|
||||||
|
sandbox: SandboxConfig | undefined,
|
||||||
|
workspace: string,
|
||||||
|
): SandboxManager {
|
||||||
|
const isWindows = os.platform() === 'win32';
|
||||||
|
|
||||||
|
if (
|
||||||
|
isWindows &&
|
||||||
|
(sandbox?.enabled || sandbox?.command === 'windows-native')
|
||||||
|
) {
|
||||||
|
return new WindowsSandboxManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sandbox?.enabled) {
|
||||||
|
if (os.platform() === 'linux') {
|
||||||
|
return new LinuxSandboxManager({ workspace });
|
||||||
|
}
|
||||||
|
if (os.platform() === 'darwin') {
|
||||||
|
return new MacOsSandboxManager({ workspace });
|
||||||
|
}
|
||||||
|
return new LocalSandboxManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NoopSandboxManager();
|
||||||
|
}
|
||||||
133
packages/core/src/services/sandboxedFileSystemService.test.ts
Normal file
133
packages/core/src/services/sandboxedFileSystemService.test.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
describe,
|
||||||
|
it,
|
||||||
|
expect,
|
||||||
|
vi,
|
||||||
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
|
type Mock,
|
||||||
|
} from 'vitest';
|
||||||
|
import { SandboxedFileSystemService } from './sandboxedFileSystemService.js';
|
||||||
|
import type {
|
||||||
|
SandboxManager,
|
||||||
|
SandboxRequest,
|
||||||
|
SandboxedCommand,
|
||||||
|
} from './sandboxManager.js';
|
||||||
|
import { spawn, type ChildProcess } from 'node:child_process';
|
||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import type { Writable } from 'node:stream';
|
||||||
|
|
||||||
|
vi.mock('node:child_process', () => ({
|
||||||
|
spawn: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
class MockSandboxManager implements SandboxManager {
|
||||||
|
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
|
||||||
|
return {
|
||||||
|
program: 'sandbox.exe',
|
||||||
|
args: ['0', req.cwd, req.command, ...req.args],
|
||||||
|
env: req.env || {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SandboxedFileSystemService', () => {
|
||||||
|
let sandboxManager: MockSandboxManager;
|
||||||
|
let service: SandboxedFileSystemService;
|
||||||
|
const cwd = '/test/cwd';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandboxManager = new MockSandboxManager();
|
||||||
|
service = new SandboxedFileSystemService(sandboxManager, cwd);
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read a file through the sandbox', async () => {
|
||||||
|
const mockChild = new EventEmitter() as unknown as ChildProcess;
|
||||||
|
Object.assign(mockChild, {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(spawn).mockReturnValue(mockChild);
|
||||||
|
|
||||||
|
const readPromise = service.readTextFile('/test/file.txt');
|
||||||
|
|
||||||
|
// Use setImmediate to ensure events are emitted after the promise starts executing
|
||||||
|
setImmediate(() => {
|
||||||
|
mockChild.stdout!.emit('data', Buffer.from('file content'));
|
||||||
|
mockChild.emit('close', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = await readPromise;
|
||||||
|
expect(content).toBe('file content');
|
||||||
|
expect(spawn).toHaveBeenCalledWith(
|
||||||
|
'sandbox.exe',
|
||||||
|
['0', cwd, '__read', '/test/file.txt'],
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write a file through the sandbox', async () => {
|
||||||
|
const mockChild = new EventEmitter() as unknown as ChildProcess;
|
||||||
|
const mockStdin = new EventEmitter();
|
||||||
|
Object.assign(mockStdin, {
|
||||||
|
write: vi.fn(),
|
||||||
|
end: vi.fn(),
|
||||||
|
});
|
||||||
|
Object.assign(mockChild, {
|
||||||
|
stdin: mockStdin as unknown as Writable,
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(spawn).mockReturnValue(mockChild);
|
||||||
|
|
||||||
|
const writePromise = service.writeTextFile('/test/file.txt', 'new content');
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
mockChild.emit('close', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await writePromise;
|
||||||
|
expect(
|
||||||
|
(mockStdin as unknown as { write: Mock }).write,
|
||||||
|
).toHaveBeenCalledWith('new content');
|
||||||
|
expect((mockStdin as unknown as { end: Mock }).end).toHaveBeenCalled();
|
||||||
|
expect(spawn).toHaveBeenCalledWith(
|
||||||
|
'sandbox.exe',
|
||||||
|
['0', cwd, '__write', '/test/file.txt'],
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject if sandbox command fails', async () => {
|
||||||
|
const mockChild = new EventEmitter() as unknown as ChildProcess;
|
||||||
|
Object.assign(mockChild, {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(spawn).mockReturnValue(mockChild);
|
||||||
|
|
||||||
|
const readPromise = service.readTextFile('/test/file.txt');
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
mockChild.stderr!.emit('data', Buffer.from('access denied'));
|
||||||
|
mockChild.emit('close', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(readPromise).rejects.toThrow(
|
||||||
|
"Sandbox Error: read_file failed for '/test/file.txt'. Exit code 1. Details: access denied",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
128
packages/core/src/services/sandboxedFileSystemService.ts
Normal file
128
packages/core/src/services/sandboxedFileSystemService.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { type FileSystemService } from './fileSystemService.js';
|
||||||
|
import { type SandboxManager } from './sandboxManager.js';
|
||||||
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
import { isNodeError } from '../utils/errors.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A FileSystemService implementation that performs operations through a sandbox.
|
||||||
|
*/
|
||||||
|
export class SandboxedFileSystemService implements FileSystemService {
|
||||||
|
constructor(
|
||||||
|
private sandboxManager: SandboxManager,
|
||||||
|
private cwd: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async readTextFile(filePath: string): Promise<string> {
|
||||||
|
const prepared = await this.sandboxManager.prepareCommand({
|
||||||
|
command: '__read',
|
||||||
|
args: [filePath],
|
||||||
|
cwd: this.cwd,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Direct spawn is necessary here for streaming large file contents.
|
||||||
|
|
||||||
|
const child = spawn(prepared.program, prepared.args, {
|
||||||
|
cwd: this.cwd,
|
||||||
|
env: prepared.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
let error = '';
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
error += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(output);
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Sandbox Error: read_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Sandbox Error: Failed to spawn read_file for '${filePath}': ${err.message}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeTextFile(filePath: string, content: string): Promise<void> {
|
||||||
|
const prepared = await this.sandboxManager.prepareCommand({
|
||||||
|
command: '__write',
|
||||||
|
args: [filePath],
|
||||||
|
cwd: this.cwd,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Direct spawn is necessary here for streaming large file contents.
|
||||||
|
|
||||||
|
const child = spawn(prepared.program, prepared.args, {
|
||||||
|
cwd: this.cwd,
|
||||||
|
env: prepared.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdin?.on('error', (err) => {
|
||||||
|
// Silently ignore EPIPE errors on stdin, they will be caught by the process error/close listeners
|
||||||
|
if (isNodeError(err) && err.code === 'EPIPE') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugLogger.error(
|
||||||
|
`Sandbox Error: stdin error for '${filePath}': ${
|
||||||
|
err instanceof Error ? err.message : String(err)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdin?.write(content);
|
||||||
|
child.stdin?.end();
|
||||||
|
|
||||||
|
let error = '';
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
error += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Sandbox Error: write_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Sandbox Error: Failed to spawn write_file for '${filePath}': ${err.message}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
370
packages/core/src/services/scripts/GeminiSandbox.cs
Normal file
370
packages/core/src/services/scripts/GeminiSandbox.cs
Normal file
|
|
@ -0,0 +1,370 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
public class GeminiSandbox {
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct STARTUPINFO {
|
||||||
|
public uint cb;
|
||||||
|
public string lpReserved;
|
||||||
|
public string lpDesktop;
|
||||||
|
public string lpTitle;
|
||||||
|
public uint dwX;
|
||||||
|
public uint dwY;
|
||||||
|
public uint dwXSize;
|
||||||
|
public uint dwYSize;
|
||||||
|
public uint dwXCountChars;
|
||||||
|
public uint dwYCountChars;
|
||||||
|
public uint dwFillAttribute;
|
||||||
|
public uint dwFlags;
|
||||||
|
public ushort wShowWindow;
|
||||||
|
public ushort cbReserved2;
|
||||||
|
public IntPtr lpReserved2;
|
||||||
|
public IntPtr hStdInput;
|
||||||
|
public IntPtr hStdOutput;
|
||||||
|
public IntPtr hStdError;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct PROCESS_INFORMATION {
|
||||||
|
public IntPtr hProcess;
|
||||||
|
public IntPtr hThread;
|
||||||
|
public uint dwProcessId;
|
||||||
|
public uint dwThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION {
|
||||||
|
public Int64 PerProcessUserTimeLimit;
|
||||||
|
public Int64 PerJobUserTimeLimit;
|
||||||
|
public uint LimitFlags;
|
||||||
|
public UIntPtr MinimumWorkingSetSize;
|
||||||
|
public UIntPtr MaximumWorkingSetSize;
|
||||||
|
public uint ActiveProcessLimit;
|
||||||
|
public UIntPtr Affinity;
|
||||||
|
public uint PriorityClass;
|
||||||
|
public uint SchedulingClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct IO_COUNTERS {
|
||||||
|
public ulong ReadOperationCount;
|
||||||
|
public ulong WriteOperationCount;
|
||||||
|
public ulong OtherOperationCount;
|
||||||
|
public ulong ReadTransferCount;
|
||||||
|
public ulong WriteTransferCount;
|
||||||
|
public ulong OtherTransferCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
|
||||||
|
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
||||||
|
public IO_COUNTERS IoInfo;
|
||||||
|
public UIntPtr ProcessMemoryLimit;
|
||||||
|
public UIntPtr JobMemoryLimit;
|
||||||
|
public UIntPtr PeakProcessMemoryUsed;
|
||||||
|
public UIntPtr PeakJobMemoryUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct SID_AND_ATTRIBUTES {
|
||||||
|
public IntPtr Sid;
|
||||||
|
public uint Attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct TOKEN_MANDATORY_LABEL {
|
||||||
|
public SID_AND_ATTRIBUTES Label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum JobObjectInfoClass {
|
||||||
|
ExtendedLimitInformation = 9
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true)]
|
||||||
|
public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true)]
|
||||||
|
public static extern bool CreateRestrictedToken(IntPtr ExistingTokenHandle, uint Flags, uint DisableSidCount, IntPtr SidsToDisable, uint DeletePrivilegeCount, IntPtr PrivilegesToDelete, uint RestrictedSidCount, IntPtr SidsToRestrict, out IntPtr NewTokenHandle);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
public static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
public static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoClass JobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern uint ResumeThread(IntPtr hThread);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool CloseHandle(IntPtr hObject);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GetStdHandle(int nStdHandle);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
public static extern bool ConvertStringSidToSid(string StringSid, out IntPtr Sid);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SetTokenInformation(IntPtr TokenHandle, int TokenInformationClass, IntPtr TokenInformation, uint TokenInformationLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr LocalFree(IntPtr hMem);
|
||||||
|
|
||||||
|
public const uint TOKEN_DUPLICATE = 0x0002;
|
||||||
|
public const uint TOKEN_QUERY = 0x0008;
|
||||||
|
public const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
|
||||||
|
public const uint TOKEN_ADJUST_DEFAULT = 0x0080;
|
||||||
|
public const uint DISABLE_MAX_PRIVILEGE = 0x1;
|
||||||
|
public const uint CREATE_SUSPENDED = 0x00000004;
|
||||||
|
public const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
|
||||||
|
public const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||||
|
public const uint STARTF_USESTDHANDLES = 0x00000100;
|
||||||
|
public const int TokenIntegrityLevel = 25;
|
||||||
|
public const uint SE_GROUP_INTEGRITY = 0x00000020;
|
||||||
|
public const uint INFINITE = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
static int Main(string[] args) {
|
||||||
|
if (args.Length < 3) {
|
||||||
|
Console.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> <command> [args...]");
|
||||||
|
Console.WriteLine("Internal commands: __read <path>, __write <path>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool networkAccess = args[0] == "1";
|
||||||
|
string cwd = args[1];
|
||||||
|
string command = args[2];
|
||||||
|
|
||||||
|
IntPtr hToken = IntPtr.Zero;
|
||||||
|
IntPtr hRestrictedToken = IntPtr.Zero;
|
||||||
|
IntPtr hJob = IntPtr.Zero;
|
||||||
|
IntPtr pSidsToDisable = IntPtr.Zero;
|
||||||
|
IntPtr pSidsToRestrict = IntPtr.Zero;
|
||||||
|
IntPtr networkSid = IntPtr.Zero;
|
||||||
|
IntPtr restrictedSid = IntPtr.Zero;
|
||||||
|
IntPtr lowIntegritySid = IntPtr.Zero;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Setup Token
|
||||||
|
IntPtr hCurrentProcess = GetCurrentProcess();
|
||||||
|
if (!OpenProcessToken(hCurrentProcess, TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_DEFAULT, out hToken)) {
|
||||||
|
Console.Error.WriteLine("Failed to open process token");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint sidCount = 0;
|
||||||
|
uint restrictCount = 0;
|
||||||
|
|
||||||
|
// "networkAccess == false" implies Strict Sandbox Level 1.
|
||||||
|
if (!networkAccess) {
|
||||||
|
if (ConvertStringSidToSid("S-1-5-2", out networkSid)) {
|
||||||
|
sidCount = 1;
|
||||||
|
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
|
||||||
|
pSidsToDisable = Marshal.AllocHGlobal(saaSize);
|
||||||
|
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
|
||||||
|
saa.Sid = networkSid;
|
||||||
|
saa.Attributes = 0;
|
||||||
|
Marshal.StructureToPtr(saa, pSidsToDisable, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// S-1-5-12 is Restricted Code SID
|
||||||
|
if (ConvertStringSidToSid("S-1-5-12", out restrictedSid)) {
|
||||||
|
restrictCount = 1;
|
||||||
|
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
|
||||||
|
pSidsToRestrict = Marshal.AllocHGlobal(saaSize);
|
||||||
|
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
|
||||||
|
saa.Sid = restrictedSid;
|
||||||
|
saa.Attributes = 0;
|
||||||
|
Marshal.StructureToPtr(saa, pSidsToRestrict, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, sidCount, pSidsToDisable, 0, IntPtr.Zero, restrictCount, pSidsToRestrict, out hRestrictedToken)) {
|
||||||
|
Console.Error.WriteLine("Failed to create restricted token");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Set Integrity Level to Low
|
||||||
|
if (ConvertStringSidToSid("S-1-16-4096", out lowIntegritySid)) {
|
||||||
|
TOKEN_MANDATORY_LABEL tml = new TOKEN_MANDATORY_LABEL();
|
||||||
|
tml.Label.Sid = lowIntegritySid;
|
||||||
|
tml.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||||
|
int tmlSize = Marshal.SizeOf(tml);
|
||||||
|
IntPtr pTml = Marshal.AllocHGlobal(tmlSize);
|
||||||
|
try {
|
||||||
|
Marshal.StructureToPtr(tml, pTml, false);
|
||||||
|
SetTokenInformation(hRestrictedToken, TokenIntegrityLevel, pTml, (uint)tmlSize);
|
||||||
|
} finally {
|
||||||
|
Marshal.FreeHGlobal(pTml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Handle Internal Commands or External Process
|
||||||
|
if (command == "__read") {
|
||||||
|
string path = args[3];
|
||||||
|
return RunInImpersonation(hRestrictedToken, () => {
|
||||||
|
try {
|
||||||
|
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||||
|
using (StreamReader sr = new StreamReader(fs, System.Text.Encoding.UTF8)) {
|
||||||
|
char[] buffer = new char[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = sr.Read(buffer, 0, buffer.Length)) > 0) {
|
||||||
|
Console.Write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Console.Error.WriteLine(e.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (command == "__write") {
|
||||||
|
string path = args[3];
|
||||||
|
return RunInImpersonation(hRestrictedToken, () => {
|
||||||
|
try {
|
||||||
|
using (StreamReader reader = new StreamReader(Console.OpenStandardInput(), System.Text.Encoding.UTF8))
|
||||||
|
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
using (StreamWriter writer = new StreamWriter(fs, System.Text.Encoding.UTF8)) {
|
||||||
|
char[] buffer = new char[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = reader.Read(buffer, 0, buffer.Length)) > 0) {
|
||||||
|
writer.Write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Console.Error.WriteLine(e.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Setup Job Object for external process
|
||||||
|
hJob = CreateJobObject(IntPtr.Zero, null);
|
||||||
|
if (hJob != IntPtr.Zero) {
|
||||||
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
|
||||||
|
limitInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||||
|
int limitSize = Marshal.SizeOf(limitInfo);
|
||||||
|
IntPtr pLimit = Marshal.AllocHGlobal(limitSize);
|
||||||
|
try {
|
||||||
|
Marshal.StructureToPtr(limitInfo, pLimit, false);
|
||||||
|
SetInformationJobObject(hJob, JobObjectInfoClass.ExtendedLimitInformation, pLimit, (uint)limitSize);
|
||||||
|
} finally {
|
||||||
|
Marshal.FreeHGlobal(pLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Launch Process
|
||||||
|
STARTUPINFO si = new STARTUPINFO();
|
||||||
|
si.cb = (uint)Marshal.SizeOf(si);
|
||||||
|
si.dwFlags = STARTF_USESTDHANDLES;
|
||||||
|
si.hStdInput = GetStdHandle(-10);
|
||||||
|
si.hStdOutput = GetStdHandle(-11);
|
||||||
|
si.hStdError = GetStdHandle(-12);
|
||||||
|
|
||||||
|
string commandLine = "";
|
||||||
|
for (int i = 2; i < args.Length; i++) {
|
||||||
|
if (i > 2) commandLine += " ";
|
||||||
|
commandLine += QuoteArgument(args[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
PROCESS_INFORMATION pi;
|
||||||
|
if (!CreateProcessAsUser(hRestrictedToken, null, commandLine, IntPtr.Zero, IntPtr.Zero, true, CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT, IntPtr.Zero, cwd, ref si, out pi)) {
|
||||||
|
Console.Error.WriteLine("Failed to create process. Error: " + Marshal.GetLastWin32Error());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (hJob != IntPtr.Zero) {
|
||||||
|
AssignProcessToJobObject(hJob, pi.hProcess);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResumeThread(pi.hThread);
|
||||||
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||||
|
|
||||||
|
uint exitCode = 0;
|
||||||
|
GetExitCodeProcess(pi.hProcess, out exitCode);
|
||||||
|
return (int)exitCode;
|
||||||
|
} finally {
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Console.Error.WriteLine("Unexpected error: " + e.Message);
|
||||||
|
return 1;
|
||||||
|
} finally {
|
||||||
|
if (hRestrictedToken != IntPtr.Zero) CloseHandle(hRestrictedToken);
|
||||||
|
if (hToken != IntPtr.Zero) CloseHandle(hToken);
|
||||||
|
if (hJob != IntPtr.Zero) CloseHandle(hJob);
|
||||||
|
if (pSidsToDisable != IntPtr.Zero) Marshal.FreeHGlobal(pSidsToDisable);
|
||||||
|
if (pSidsToRestrict != IntPtr.Zero) Marshal.FreeHGlobal(pSidsToRestrict);
|
||||||
|
if (networkSid != IntPtr.Zero) LocalFree(networkSid);
|
||||||
|
if (restrictedSid != IntPtr.Zero) LocalFree(restrictedSid);
|
||||||
|
if (lowIntegritySid != IntPtr.Zero) LocalFree(lowIntegritySid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string QuoteArgument(string arg) {
|
||||||
|
if (string.IsNullOrEmpty(arg)) return "\"\"";
|
||||||
|
|
||||||
|
bool hasSpace = arg.IndexOfAny(new char[] { ' ', '\t' }) != -1;
|
||||||
|
if (!hasSpace && arg.IndexOf('\"') == -1) return arg;
|
||||||
|
|
||||||
|
// Windows command line escaping for arguments is complex.
|
||||||
|
// Rule: Backslashes only need escaping if they precede a double quote or the end of the string.
|
||||||
|
System.Text.StringBuilder sb = new System.Text.StringBuilder();
|
||||||
|
sb.Append('\"');
|
||||||
|
for (int i = 0; i < arg.Length; i++) {
|
||||||
|
int backslashCount = 0;
|
||||||
|
while (i < arg.Length && arg[i] == '\\') {
|
||||||
|
backslashCount++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == arg.Length) {
|
||||||
|
// Escape backslashes before the closing double quote
|
||||||
|
sb.Append('\\', backslashCount * 2);
|
||||||
|
} else if (arg[i] == '\"') {
|
||||||
|
// Escape backslashes before a literal double quote
|
||||||
|
sb.Append('\\', backslashCount * 2 + 1);
|
||||||
|
sb.Append('\"');
|
||||||
|
} else {
|
||||||
|
// Backslashes don't need escaping here
|
||||||
|
sb.Append('\\', backslashCount);
|
||||||
|
sb.Append(arg[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.Append('\"');
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunInImpersonation(IntPtr hToken, Func<int> action) {
|
||||||
|
using (WindowsIdentity.Impersonate(hToken)) {
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,8 +27,12 @@ import {
|
||||||
serializeTerminalToObject,
|
serializeTerminalToObject,
|
||||||
type AnsiOutput,
|
type AnsiOutput,
|
||||||
} from '../utils/terminalSerializer.js';
|
} from '../utils/terminalSerializer.js';
|
||||||
import { type EnvironmentSanitizationConfig } from './environmentSanitization.js';
|
import {
|
||||||
import { type SandboxManager } from './sandboxManager.js';
|
sanitizeEnvironment,
|
||||||
|
type EnvironmentSanitizationConfig,
|
||||||
|
} from './environmentSanitization.js';
|
||||||
|
import { NoopSandboxManager, type SandboxManager } from './sandboxManager.js';
|
||||||
|
import type { SandboxConfig } from '../config/config.js';
|
||||||
import { killProcessGroup } from '../utils/process-utils.js';
|
import { killProcessGroup } from '../utils/process-utils.js';
|
||||||
import {
|
import {
|
||||||
ExecutionLifecycleService,
|
ExecutionLifecycleService,
|
||||||
|
|
@ -92,6 +96,7 @@ export interface ShellExecutionConfig {
|
||||||
disableDynamicLineTrimming?: boolean;
|
disableDynamicLineTrimming?: boolean;
|
||||||
scrollback?: number;
|
scrollback?: number;
|
||||||
maxSerializedLines?: number;
|
maxSerializedLines?: number;
|
||||||
|
sandboxConfig?: SandboxConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -331,37 +336,119 @@ export class ShellExecutionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async prepareExecution(
|
private static async prepareExecution(
|
||||||
executable: string,
|
commandToExecute: string,
|
||||||
args: string[],
|
|
||||||
cwd: string,
|
cwd: string,
|
||||||
env: NodeJS.ProcessEnv,
|
|
||||||
shellExecutionConfig: ShellExecutionConfig,
|
shellExecutionConfig: ShellExecutionConfig,
|
||||||
sanitizationConfigOverride?: EnvironmentSanitizationConfig,
|
isInteractive: boolean,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
program: string;
|
program: string;
|
||||||
args: string[];
|
args: string[];
|
||||||
env: NodeJS.ProcessEnv;
|
env: Record<string, string | undefined>;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
}> {
|
}> {
|
||||||
|
const sandboxManager =
|
||||||
|
shellExecutionConfig.sandboxManager ?? new NoopSandboxManager();
|
||||||
|
|
||||||
|
// 1. Determine Shell Configuration
|
||||||
|
const isWindows = os.platform() === 'win32';
|
||||||
|
const isStrictSandbox =
|
||||||
|
isWindows &&
|
||||||
|
shellExecutionConfig.sandboxConfig?.enabled &&
|
||||||
|
shellExecutionConfig.sandboxConfig?.command === 'windows-native' &&
|
||||||
|
!shellExecutionConfig.sandboxConfig?.networkAccess;
|
||||||
|
|
||||||
|
let { executable, argsPrefix, shell } = getShellConfiguration();
|
||||||
|
if (isStrictSandbox) {
|
||||||
|
shell = 'cmd';
|
||||||
|
argsPrefix = ['/c'];
|
||||||
|
executable = 'cmd.exe';
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedExecutable =
|
const resolvedExecutable =
|
||||||
(await resolveExecutable(executable)) ?? executable;
|
(await resolveExecutable(executable)) ?? executable;
|
||||||
|
|
||||||
const prepared = await shellExecutionConfig.sandboxManager.prepareCommand({
|
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
||||||
|
const spawnArgs = [...argsPrefix, guardedCommand];
|
||||||
|
|
||||||
|
// 2. Prepare Environment
|
||||||
|
const gitConfigKeys: string[] = [];
|
||||||
|
if (!isInteractive) {
|
||||||
|
for (const key in process.env) {
|
||||||
|
if (key.startsWith('GIT_CONFIG_')) {
|
||||||
|
gitConfigKeys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizationConfig = {
|
||||||
|
...shellExecutionConfig.sanitizationConfig,
|
||||||
|
allowedEnvironmentVariables: [
|
||||||
|
...(shellExecutionConfig.sanitizationConfig
|
||||||
|
.allowedEnvironmentVariables || []),
|
||||||
|
...gitConfigKeys,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizedEnv = sanitizeEnvironment(process.env, sanitizationConfig);
|
||||||
|
|
||||||
|
const baseEnv: Record<string, string | undefined> = {
|
||||||
|
...sanitizedEnv,
|
||||||
|
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
|
||||||
|
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
|
||||||
|
TERM: 'xterm-256color',
|
||||||
|
PAGER: shellExecutionConfig.pager ?? 'cat',
|
||||||
|
GIT_PAGER: shellExecutionConfig.pager ?? 'cat',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isInteractive) {
|
||||||
|
// Ensure all GIT_CONFIG_* variables are preserved even if they were redacted
|
||||||
|
for (const key of gitConfigKeys) {
|
||||||
|
baseEnv[key] = process.env[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const gitConfigCount = parseInt(baseEnv['GIT_CONFIG_COUNT'] || '0', 10);
|
||||||
|
const newKey = `GIT_CONFIG_KEY_${gitConfigCount}`;
|
||||||
|
const newValue = `GIT_CONFIG_VALUE_${gitConfigCount}`;
|
||||||
|
|
||||||
|
// Ensure these new keys are allowed through sanitization
|
||||||
|
sanitizationConfig.allowedEnvironmentVariables.push(
|
||||||
|
'GIT_CONFIG_COUNT',
|
||||||
|
newKey,
|
||||||
|
newValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(baseEnv, {
|
||||||
|
GIT_TERMINAL_PROMPT: '0',
|
||||||
|
GIT_ASKPASS: '',
|
||||||
|
SSH_ASKPASS: '',
|
||||||
|
GH_PROMPT_DISABLED: '1',
|
||||||
|
GCM_INTERACTIVE: 'never',
|
||||||
|
DISPLAY: '',
|
||||||
|
DBUS_SESSION_BUS_ADDRESS: '',
|
||||||
|
GIT_CONFIG_COUNT: (gitConfigCount + 1).toString(),
|
||||||
|
[newKey]: 'credential.helper',
|
||||||
|
[newValue]: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Prepare Sandboxed Command
|
||||||
|
const sandboxedCommand = await sandboxManager.prepareCommand({
|
||||||
command: resolvedExecutable,
|
command: resolvedExecutable,
|
||||||
args,
|
args: spawnArgs,
|
||||||
|
env: baseEnv,
|
||||||
cwd,
|
cwd,
|
||||||
env,
|
|
||||||
config: {
|
config: {
|
||||||
sanitizationConfig:
|
...shellExecutionConfig,
|
||||||
sanitizationConfigOverride ?? shellExecutionConfig.sanitizationConfig,
|
...(shellExecutionConfig.sandboxConfig || {}),
|
||||||
|
sanitizationConfig,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
program: prepared.program,
|
program: sandboxedCommand.program,
|
||||||
args: prepared.args,
|
args: sandboxedCommand.args,
|
||||||
env: prepared.env,
|
env: sandboxedCommand.env,
|
||||||
cwd: prepared.cwd ?? cwd,
|
cwd: sandboxedCommand.cwd ?? cwd,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,70 +462,19 @@ export class ShellExecutionService {
|
||||||
): Promise<ShellExecutionHandle> {
|
): Promise<ShellExecutionHandle> {
|
||||||
try {
|
try {
|
||||||
const isWindows = os.platform() === 'win32';
|
const isWindows = os.platform() === 'win32';
|
||||||
const { executable, argsPrefix, shell } = getShellConfiguration();
|
|
||||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
|
||||||
const spawnArgs = [...argsPrefix, guardedCommand];
|
|
||||||
|
|
||||||
// Specifically allow GIT_CONFIG_* variables to pass through sanitization
|
|
||||||
// in non-interactive mode so we can safely append our overrides.
|
|
||||||
const gitConfigKeys = !isInteractive
|
|
||||||
? Object.keys(process.env).filter((k) => k.startsWith('GIT_CONFIG_'))
|
|
||||||
: [];
|
|
||||||
const localSanitizationConfig = {
|
|
||||||
...shellExecutionConfig.sanitizationConfig,
|
|
||||||
allowedEnvironmentVariables: [
|
|
||||||
...(shellExecutionConfig.sanitizationConfig
|
|
||||||
.allowedEnvironmentVariables || []),
|
|
||||||
...gitConfigKeys,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
|
|
||||||
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
|
|
||||||
TERM: 'xterm-256color',
|
|
||||||
PAGER: 'cat',
|
|
||||||
GIT_PAGER: 'cat',
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
program: finalExecutable,
|
program: finalExecutable,
|
||||||
args: finalArgs,
|
args: finalArgs,
|
||||||
env: sanitizedEnv,
|
env: finalEnv,
|
||||||
cwd: finalCwd,
|
cwd: finalCwd,
|
||||||
} = await this.prepareExecution(
|
} = await this.prepareExecution(
|
||||||
executable,
|
commandToExecute,
|
||||||
spawnArgs,
|
|
||||||
cwd,
|
cwd,
|
||||||
env,
|
|
||||||
shellExecutionConfig,
|
shellExecutionConfig,
|
||||||
localSanitizationConfig,
|
isInteractive,
|
||||||
);
|
);
|
||||||
|
|
||||||
const finalEnv = { ...sanitizedEnv };
|
|
||||||
|
|
||||||
if (!isInteractive) {
|
|
||||||
const gitConfigCount = parseInt(
|
|
||||||
finalEnv['GIT_CONFIG_COUNT'] || '0',
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
Object.assign(finalEnv, {
|
|
||||||
// Disable interactive prompts and session-linked credential helpers
|
|
||||||
// in non-interactive mode to prevent hangs in detached process groups.
|
|
||||||
GIT_TERMINAL_PROMPT: '0',
|
|
||||||
GIT_ASKPASS: '',
|
|
||||||
SSH_ASKPASS: '',
|
|
||||||
GH_PROMPT_DISABLED: '1',
|
|
||||||
GCM_INTERACTIVE: 'never',
|
|
||||||
DISPLAY: '',
|
|
||||||
DBUS_SESSION_BUS_ADDRESS: '',
|
|
||||||
GIT_CONFIG_COUNT: (gitConfigCount + 1).toString(),
|
|
||||||
[`GIT_CONFIG_KEY_${gitConfigCount}`]: 'credential.helper',
|
|
||||||
[`GIT_CONFIG_VALUE_${gitConfigCount}`]: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const child = cpSpawn(finalExecutable, finalArgs, {
|
const child = cpSpawn(finalExecutable, finalArgs, {
|
||||||
cwd: finalCwd,
|
cwd: finalCwd,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
|
@ -732,32 +768,6 @@ export class ShellExecutionService {
|
||||||
try {
|
try {
|
||||||
const cols = shellExecutionConfig.terminalWidth ?? 80;
|
const cols = shellExecutionConfig.terminalWidth ?? 80;
|
||||||
const rows = shellExecutionConfig.terminalHeight ?? 30;
|
const rows = shellExecutionConfig.terminalHeight ?? 30;
|
||||||
const { executable, argsPrefix, shell } = getShellConfiguration();
|
|
||||||
|
|
||||||
const guardedCommand = ensurePromptvarsDisabled(commandToExecute, shell);
|
|
||||||
const args = [...argsPrefix, guardedCommand];
|
|
||||||
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
GEMINI_CLI: '1',
|
|
||||||
TERM: 'xterm-256color',
|
|
||||||
PAGER: shellExecutionConfig.pager ?? 'cat',
|
|
||||||
GIT_PAGER: shellExecutionConfig.pager ?? 'cat',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Specifically allow GIT_CONFIG_* variables to pass through sanitization
|
|
||||||
// so we can safely append our overrides if needed.
|
|
||||||
const gitConfigKeys = Object.keys(process.env).filter((k) =>
|
|
||||||
k.startsWith('GIT_CONFIG_'),
|
|
||||||
);
|
|
||||||
const localSanitizationConfig = {
|
|
||||||
...shellExecutionConfig.sanitizationConfig,
|
|
||||||
allowedEnvironmentVariables: [
|
|
||||||
...(shellExecutionConfig.sanitizationConfig
|
|
||||||
?.allowedEnvironmentVariables ?? []),
|
|
||||||
...gitConfigKeys,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
program: finalExecutable,
|
program: finalExecutable,
|
||||||
|
|
@ -765,12 +775,10 @@ export class ShellExecutionService {
|
||||||
env: finalEnv,
|
env: finalEnv,
|
||||||
cwd: finalCwd,
|
cwd: finalCwd,
|
||||||
} = await this.prepareExecution(
|
} = await this.prepareExecution(
|
||||||
executable,
|
commandToExecute,
|
||||||
args,
|
|
||||||
cwd,
|
cwd,
|
||||||
env,
|
|
||||||
shellExecutionConfig,
|
shellExecutionConfig,
|
||||||
localSanitizationConfig,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
|
@ -782,6 +790,7 @@ export class ShellExecutionService {
|
||||||
env: finalEnv,
|
env: finalEnv,
|
||||||
handleFlowControl: true,
|
handleFlowControl: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
spawnedPty = ptyProcess as IPty;
|
spawnedPty = ptyProcess as IPty;
|
||||||
const ptyPid = Number(ptyProcess.pid);
|
const ptyPid = Number(ptyProcess.pid);
|
||||||
|
|
|
||||||
68
packages/core/src/services/windowsSandboxManager.test.ts
Normal file
68
packages/core/src/services/windowsSandboxManager.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { WindowsSandboxManager } from './windowsSandboxManager.js';
|
||||||
|
import type { SandboxRequest } from './sandboxManager.js';
|
||||||
|
|
||||||
|
describe('WindowsSandboxManager', () => {
|
||||||
|
const manager = new WindowsSandboxManager('win32');
|
||||||
|
|
||||||
|
it('should prepare a GeminiSandbox.exe command', async () => {
|
||||||
|
const req: SandboxRequest = {
|
||||||
|
command: 'whoami',
|
||||||
|
args: ['/groups'],
|
||||||
|
cwd: '/test/cwd',
|
||||||
|
env: { TEST_VAR: 'test_value' },
|
||||||
|
config: {
|
||||||
|
networkAccess: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await manager.prepareCommand(req);
|
||||||
|
|
||||||
|
expect(result.program).toContain('GeminiSandbox.exe');
|
||||||
|
expect(result.args).toEqual(['0', '/test/cwd', 'whoami', '/groups']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle networkAccess from config', async () => {
|
||||||
|
const req: SandboxRequest = {
|
||||||
|
command: 'whoami',
|
||||||
|
args: [],
|
||||||
|
cwd: '/test/cwd',
|
||||||
|
env: {},
|
||||||
|
config: {
|
||||||
|
networkAccess: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await manager.prepareCommand(req);
|
||||||
|
expect(result.args[0]).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sanitize environment variables', async () => {
|
||||||
|
const req: SandboxRequest = {
|
||||||
|
command: 'test',
|
||||||
|
args: [],
|
||||||
|
cwd: '/test/cwd',
|
||||||
|
env: {
|
||||||
|
API_KEY: 'secret',
|
||||||
|
PATH: '/usr/bin',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
sanitizationConfig: {
|
||||||
|
allowedEnvironmentVariables: ['PATH'],
|
||||||
|
blockedEnvironmentVariables: ['API_KEY'],
|
||||||
|
enableEnvironmentVariableRedaction: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await manager.prepareCommand(req);
|
||||||
|
expect(result.env['PATH']).toBe('/usr/bin');
|
||||||
|
expect(result.env['API_KEY']).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
228
packages/core/src/services/windowsSandboxManager.ts
Normal file
228
packages/core/src/services/windowsSandboxManager.ts
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import type {
|
||||||
|
SandboxManager,
|
||||||
|
SandboxRequest,
|
||||||
|
SandboxedCommand,
|
||||||
|
} from './sandboxManager.js';
|
||||||
|
import {
|
||||||
|
sanitizeEnvironment,
|
||||||
|
type EnvironmentSanitizationConfig,
|
||||||
|
} from './environmentSanitization.js';
|
||||||
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
import { spawnAsync } from '../utils/shell-utils.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SandboxManager implementation for Windows that uses Restricted Tokens,
|
||||||
|
* Job Objects, and Low Integrity levels for process isolation.
|
||||||
|
* Uses a native C# helper to bypass PowerShell restrictions.
|
||||||
|
*/
|
||||||
|
export class WindowsSandboxManager implements SandboxManager {
|
||||||
|
private readonly helperPath: string;
|
||||||
|
private readonly platform: string;
|
||||||
|
private initialized = false;
|
||||||
|
private readonly lowIntegrityCache = new Set<string>();
|
||||||
|
|
||||||
|
constructor(platform: string = process.platform) {
|
||||||
|
this.platform = platform;
|
||||||
|
this.helperPath = path.resolve(__dirname, 'scripts', 'GeminiSandbox.exe');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureInitialized(): Promise<void> {
|
||||||
|
if (this.initialized) return;
|
||||||
|
if (this.platform !== 'win32') {
|
||||||
|
this.initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(this.helperPath)) {
|
||||||
|
debugLogger.log(
|
||||||
|
`WindowsSandboxManager: Helper not found at ${this.helperPath}. Attempting to compile...`,
|
||||||
|
);
|
||||||
|
// If the exe doesn't exist, we try to compile it from the .cs file
|
||||||
|
const sourcePath = this.helperPath.replace(/\.exe$/, '.cs');
|
||||||
|
if (fs.existsSync(sourcePath)) {
|
||||||
|
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
|
||||||
|
const cscPaths = [
|
||||||
|
'csc.exe', // Try in PATH first
|
||||||
|
path.join(
|
||||||
|
systemRoot,
|
||||||
|
'Microsoft.NET',
|
||||||
|
'Framework64',
|
||||||
|
'v4.0.30319',
|
||||||
|
'csc.exe',
|
||||||
|
),
|
||||||
|
path.join(
|
||||||
|
systemRoot,
|
||||||
|
'Microsoft.NET',
|
||||||
|
'Framework',
|
||||||
|
'v4.0.30319',
|
||||||
|
'csc.exe',
|
||||||
|
),
|
||||||
|
// Added newer framework paths
|
||||||
|
path.join(
|
||||||
|
systemRoot,
|
||||||
|
'Microsoft.NET',
|
||||||
|
'Framework64',
|
||||||
|
'v4.8',
|
||||||
|
'csc.exe',
|
||||||
|
),
|
||||||
|
path.join(
|
||||||
|
systemRoot,
|
||||||
|
'Microsoft.NET',
|
||||||
|
'Framework',
|
||||||
|
'v4.8',
|
||||||
|
'csc.exe',
|
||||||
|
),
|
||||||
|
path.join(
|
||||||
|
systemRoot,
|
||||||
|
'Microsoft.NET',
|
||||||
|
'Framework64',
|
||||||
|
'v3.5',
|
||||||
|
'csc.exe',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let compiled = false;
|
||||||
|
for (const csc of cscPaths) {
|
||||||
|
try {
|
||||||
|
debugLogger.log(
|
||||||
|
`WindowsSandboxManager: Trying to compile using ${csc}...`,
|
||||||
|
);
|
||||||
|
// We use spawnAsync but we don't need to capture output
|
||||||
|
await spawnAsync(csc, ['/out:' + this.helperPath, sourcePath]);
|
||||||
|
debugLogger.log(
|
||||||
|
`WindowsSandboxManager: Successfully compiled sandbox helper at ${this.helperPath}`,
|
||||||
|
);
|
||||||
|
compiled = true;
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger.log(
|
||||||
|
`WindowsSandboxManager: Failed to compile using ${csc}: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!compiled) {
|
||||||
|
debugLogger.log(
|
||||||
|
'WindowsSandboxManager: Failed to compile sandbox helper from any known CSC path.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLogger.log(
|
||||||
|
`WindowsSandboxManager: Source file not found at ${sourcePath}. Cannot compile helper.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLogger.log(
|
||||||
|
`WindowsSandboxManager: Found helper at ${this.helperPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger.log(
|
||||||
|
'WindowsSandboxManager: Failed to initialize sandbox helper:',
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a command for sandboxed execution on Windows.
|
||||||
|
*/
|
||||||
|
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
|
||||||
|
const sanitizationConfig: EnvironmentSanitizationConfig = {
|
||||||
|
allowedEnvironmentVariables:
|
||||||
|
req.config?.sanitizationConfig?.allowedEnvironmentVariables ?? [],
|
||||||
|
blockedEnvironmentVariables:
|
||||||
|
req.config?.sanitizationConfig?.blockedEnvironmentVariables ?? [],
|
||||||
|
enableEnvironmentVariableRedaction:
|
||||||
|
req.config?.sanitizationConfig?.enableEnvironmentVariableRedaction ??
|
||||||
|
true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
|
||||||
|
|
||||||
|
// 1. Handle filesystem permissions for Low Integrity
|
||||||
|
// Grant "Low Mandatory Level" write access to the CWD.
|
||||||
|
await this.grantLowIntegrityAccess(req.cwd);
|
||||||
|
|
||||||
|
// Grant "Low Mandatory Level" read access to allowedPaths.
|
||||||
|
if (req.config?.allowedPaths) {
|
||||||
|
for (const allowedPath of req.config.allowedPaths) {
|
||||||
|
await this.grantLowIntegrityAccess(allowedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Construct the helper command
|
||||||
|
// GeminiSandbox.exe <network:0|1> <cwd> <command> [args...]
|
||||||
|
const program = this.helperPath;
|
||||||
|
|
||||||
|
// If the command starts with __, it's an internal command for the sandbox helper itself.
|
||||||
|
const args = [
|
||||||
|
req.config?.networkAccess ? '1' : '0',
|
||||||
|
req.cwd,
|
||||||
|
req.command,
|
||||||
|
...req.args,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
program,
|
||||||
|
args,
|
||||||
|
env: sanitizedEnv,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grants "Low Mandatory Level" access to a path using icacls.
|
||||||
|
*/
|
||||||
|
private async grantLowIntegrityAccess(targetPath: string): Promise<void> {
|
||||||
|
if (this.platform !== 'win32') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = path.resolve(targetPath);
|
||||||
|
if (this.lowIntegrityCache.has(resolvedPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Never modify integrity levels for system directories
|
||||||
|
const systemRoot = process.env['SystemRoot'] || 'C:\\Windows';
|
||||||
|
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
|
||||||
|
const programFilesX86 =
|
||||||
|
process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
||||||
|
|
||||||
|
if (
|
||||||
|
resolvedPath.toLowerCase().startsWith(systemRoot.toLowerCase()) ||
|
||||||
|
resolvedPath.toLowerCase().startsWith(programFiles.toLowerCase()) ||
|
||||||
|
resolvedPath.toLowerCase().startsWith(programFilesX86.toLowerCase())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await spawnAsync('icacls', [resolvedPath, '/setintegritylevel', 'Low']);
|
||||||
|
this.lowIntegrityCache.add(resolvedPath);
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger.log(
|
||||||
|
'WindowsSandboxManager: icacls failed for',
|
||||||
|
resolvedPath,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2251,10 +2251,27 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"title": "Sandbox",
|
"title": "Sandbox",
|
||||||
"description": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\").",
|
"description": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\", \"windows-native\").",
|
||||||
"markdownDescription": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\").\n\n- Category: `Tools`\n- Requires restart: `yes`",
|
"markdownDescription": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\", \"windows-native\").\n\n- Category: `Tools`\n- Requires restart: `yes`",
|
||||||
"$ref": "#/$defs/BooleanOrStringOrObject"
|
"$ref": "#/$defs/BooleanOrStringOrObject"
|
||||||
},
|
},
|
||||||
|
"sandboxAllowedPaths": {
|
||||||
|
"title": "Sandbox Allowed Paths",
|
||||||
|
"description": "List of additional paths that the sandbox is allowed to access.",
|
||||||
|
"markdownDescription": "List of additional paths that the sandbox is allowed to access.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `[]`",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sandboxNetworkAccess": {
|
||||||
|
"title": "Sandbox Network Access",
|
||||||
|
"description": "Whether the sandbox is allowed to access the network.",
|
||||||
|
"markdownDescription": "Whether the sandbox is allowed to access the network.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `false`",
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"shell": {
|
"shell": {
|
||||||
"title": "Shell",
|
"title": "Shell",
|
||||||
"description": "Settings for shell execution.",
|
"description": "Settings for shell execution.",
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import path from 'node:path';
|
||||||
const sourceDir = path.join('src');
|
const sourceDir = path.join('src');
|
||||||
const targetDir = path.join('dist', 'src');
|
const targetDir = path.join('dist', 'src');
|
||||||
|
|
||||||
const extensionsToCopy = ['.md', '.json', '.sb', '.toml'];
|
const extensionsToCopy = ['.md', '.json', '.sb', '.toml', '.cs', '.exe'];
|
||||||
|
|
||||||
function copyFilesRecursive(source, target) {
|
function copyFilesRecursive(source, target) {
|
||||||
if (!fs.existsSync(target)) {
|
if (!fs.existsSync(target)) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue