shouldUseSandbox.ts
tools/BashTool/shouldUseSandbox.ts
154
Lines
5185
Bytes
1
Exports
5
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is part of the tool layer, which means it describes actions the system can perform for the user or model.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with tool-system, shell-safety. It contains 154 lines, 5 detected imports, and 1 detected exports.
Important relationships
Detected exports
shouldUseSandbox
Keywords
commandcommandscandidatessandboxrulesettingschecksubstringsexcludedsubcommands
Detected imports
src/services/analytics/growthbook.js../../utils/bash/commands.js../../utils/sandbox/sandbox-adapter.js../../utils/settings/settings.js./bashPermissions.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
import { getSettings_DEPRECATED } from '../../utils/settings/settings.js'
import {
BINARY_HIJACK_VARS,
bashPermissionRule,
matchWildcardPattern,
stripAllLeadingEnvVars,
stripSafeWrappers,
} from './bashPermissions.js'
type SandboxInput = {
command?: string
dangerouslyDisableSandbox?: boolean
}
// NOTE: excludedCommands is a user-facing convenience feature, not a security boundary.
// It is not a security bug to be able to bypass excludedCommands — the sandbox permission
// system (which prompts users) is the actual security control.
function containsExcludedCommand(command: string): boolean {
// Check dynamic config for disabled commands and substrings (only for ants)
if (process.env.USER_TYPE === 'ant') {
const disabledCommands = getFeatureValue_CACHED_MAY_BE_STALE<{
commands: string[]
substrings: string[]
}>('tengu_sandbox_disabled_commands', { commands: [], substrings: [] })
// Check if command contains any disabled substrings
for (const substring of disabledCommands.substrings) {
if (command.includes(substring)) {
return true
}
}
// Check if command starts with any disabled commands
try {
const commandParts = splitCommand_DEPRECATED(command)
for (const part of commandParts) {
const baseCommand = part.trim().split(' ')[0]
if (baseCommand && disabledCommands.commands.includes(baseCommand)) {
return true
}
}
} catch {
// If we can't parse the command (e.g., malformed bash syntax),
// treat it as not excluded to allow other validation checks to handle it
// This prevents crashes when rendering tool use messages
}
}
// Check user-configured excluded commands from settings
const settings = getSettings_DEPRECATED()
const userExcludedCommands = settings.sandbox?.excludedCommands ?? []
if (userExcludedCommands.length === 0) {
return false
}
// Split compound commands (e.g. "docker ps && curl evil.com") into individual
// subcommands and check each one against excluded patterns. This prevents a
// compound command from escaping the sandbox just because its first subcommand
// matches an excluded pattern.
let subcommands: string[]
try {
subcommands = splitCommand_DEPRECATED(command)
} catch {
subcommands = [command]
}
for (const subcommand of subcommands) {
const trimmed = subcommand.trim()
// Also try matching with env var prefixes and wrapper commands stripped, so
// that `FOO=bar bazel ...` and `timeout 30 bazel ...` match `bazel:*`. Not a
// security boundary (see NOTE at top); the &&-split above already lets
// `export FOO=bar && bazel ...` match. BINARY_HIJACK_VARS kept as a heuristic.
//
// We iteratively apply both stripping operations until no new candidates are
// produced (fixed-point), matching the approach in filterRulesByContentsMatchingInput.
// This handles interleaved patterns like `timeout 300 FOO=bar bazel run`
// where single-pass composition would fail.
const candidates = [trimmed]
const seen = new Set(candidates)
let startIdx = 0
while (startIdx < candidates.length) {
const endIdx = candidates.length
for (let i = startIdx; i < endIdx; i++) {
const cmd = candidates[i]!
const envStripped = stripAllLeadingEnvVars(cmd, BINARY_HIJACK_VARS)
if (!seen.has(envStripped)) {
candidates.push(envStripped)
seen.add(envStripped)
}
const wrapperStripped = stripSafeWrappers(cmd)
if (!seen.has(wrapperStripped)) {
candidates.push(wrapperStripped)
seen.add(wrapperStripped)
}
}
startIdx = endIdx
}
for (const pattern of userExcludedCommands) {
const rule = bashPermissionRule(pattern)
for (const cand of candidates) {
switch (rule.type) {
case 'prefix':
if (cand === rule.prefix || cand.startsWith(rule.prefix + ' ')) {
return true
}
break
case 'exact':
if (cand === rule.command) {
return true
}
break
case 'wildcard':
if (matchWildcardPattern(rule.pattern, cand)) {
return true
}
break
}
}
}
}
return false
}
export function shouldUseSandbox(input: Partial<SandboxInput>): boolean {
if (!SandboxManager.isSandboxingEnabled()) {
return false
}
// Don't sandbox if explicitly overridden AND unsandboxed commands are allowed by policy
if (
input.dangerouslyDisableSandbox &&
SandboxManager.areUnsandboxedCommandsAllowed()
) {
return false
}
if (!input.command) {
return false
}
// Don't sandbox if the command contains user-configured excluded commands
if (containsExcludedCommand(input.command)) {
return false
}
return true
}