Files
viettranx a4a7a59b6a fix(sandbox): path isolation, DooD volume mounting, hints, and nginx DNS (#129, #136)
Address issues identified in PR #137 with a cleaner approach:

- Sandbox isolation: add SandboxCwd/ResolveSandboxPath helpers to map
  filesystem tool paths to agent-scoped container subdirectories,
  preventing cross-agent file access via read/write/edit/list tools
- DooD volume mounting: add resolveHostWorkspacePath with multi-strategy
  container ID detection (/proc/self/mountinfo, HOSTNAME, os.Hostname)
  and 5s timeout on docker inspect
- Sandbox hints: expand from 1 pattern (binary not found) to 6 patterns
  (permission denied, network disabled, read-only FS, missing file,
  resource limits) with MaybeFsBridgeHint for filesystem tools
- Nginx DNS: add Docker resolver (127.0.0.11) with dynamic upstream
  variable to handle backend container IP changes
- MCP args: switch from comma-separated to space-separated parsing
  with quote support for --flag="value with spaces" patterns
- Refactor: rename ExecTool.workingDir to workspace for consistency,
  extract sandbox.DefaultContainerWorkdir constant
2026-03-20 07:32:23 +07:00

116 lines
4.2 KiB
Go

package tools
import "strings"
// --- Hint messages for LLM consumption ---
const hintBinaryNotFound = "\n\n[SANDBOX] This command ran inside a Docker sandbox container. " +
"The required tool/binary is not installed in the sandbox image. " +
"Tell the user this failed due to sandbox environment limitations — " +
"they can install the binary in the sandbox image or disable sandbox mode for this agent."
const hintPermissionDenied = "\n\n[SANDBOX] Permission denied inside sandbox container. " +
"The workspace may be mounted as read-only (workspace_access: ro). " +
"Check the agent's sandbox configuration or tell the user to change workspace_access to rw."
const hintNetworkDisabled = "\n\n[SANDBOX] Network operation failed — sandbox networking is disabled (--network none). " +
"If this agent needs internet access, tell the user to enable network_enabled in the agent's sandbox configuration."
const hintReadOnlyFS = "\n\n[SANDBOX] Write failed — target path is outside the mounted workspace volume. " +
"The sandbox filesystem is read-only except for the workspace mount. " +
"Ensure all file operations use paths within the workspace directory."
const hintNoSuchFile = "\n\n[SANDBOX] File or directory not found inside sandbox container. " +
"The sandbox has a minimal filesystem — only the workspace mount and installed packages are available. " +
"Verify the path exists within the workspace, or install required files in the sandbox image."
const hintResourceLimit = "\n\n[SANDBOX] Sandbox resource limit reached (disk space or memory). " +
"The container has restricted resources. Tell the user to increase sandbox limits " +
"or clean up temporary files in the workspace."
// --- Detection functions ---
// isBinaryNotFound returns true if the error indicates a missing binary.
// Exit code 127 is the POSIX convention for "command not found".
func isBinaryNotFound(exitCode int, output string) bool {
if exitCode == 127 {
return true
}
lower := strings.ToLower(output)
return strings.Contains(lower, "not found") &&
(strings.Contains(lower, "command") || strings.Contains(lower, "sh:"))
}
func isPermissionDenied(output string) bool {
lower := strings.ToLower(output)
return strings.Contains(lower, "permission denied") || strings.Contains(lower, "eacces")
}
func isNetworkDisabled(output string) bool {
lower := strings.ToLower(output)
return strings.Contains(lower, "network is unreachable") ||
strings.Contains(lower, "name resolution")
// Note: "connection refused" is NOT matched here — it indicates a service
// not running, not network disabled. --network none produces "network is
// unreachable" or DNS resolution failures, not "connection refused".
}
func isReadOnlyFS(output string) bool {
lower := strings.ToLower(output)
return strings.Contains(lower, "read-only file system") || strings.Contains(lower, "erofs")
}
func isResourceLimit(output string) bool {
lower := strings.ToLower(output)
return strings.Contains(lower, "no space left") ||
strings.Contains(lower, "enospc") ||
strings.Contains(lower, "cannot allocate memory")
}
// --- Public API ---
// MaybeSandboxHint returns an LLM-actionable hint suffix for sandbox exec errors.
// Checks multiple error patterns in priority order. Returns empty string if no
// pattern matches. Only call from sandbox execution paths.
func MaybeSandboxHint(exitCode int, output string) string {
if isBinaryNotFound(exitCode, output) {
return hintBinaryNotFound
}
if isPermissionDenied(output) {
return hintPermissionDenied
}
if isNetworkDisabled(output) {
return hintNetworkDisabled
}
if isReadOnlyFS(output) {
return hintReadOnlyFS
}
if isResourceLimit(output) {
return hintResourceLimit
}
return ""
}
// MaybeFsBridgeHint returns an LLM-actionable hint for FsBridge errors
// (filesystem tool sandbox operations). FsBridge errors are Go errors,
// not exit codes, so we match against err.Error() text.
func MaybeFsBridgeHint(err error) string {
if err == nil {
return ""
}
msg := err.Error()
if isPermissionDenied(msg) {
return hintPermissionDenied
}
if isReadOnlyFS(msg) {
return hintReadOnlyFS
}
if strings.Contains(strings.ToLower(msg), "no such file") {
return hintNoSuchFile
}
if isResourceLimit(msg) {
return hintResourceLimit
}
return ""
}