mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
The session_start env block previously named the OS but not the shell. When the model reads 'Operating System: Windows' it defaults to POSIX syntax by habit and emits '&&', 'grep', 'head', '2>/dev/null', 'ls -la', '$(pwd)' — none of which work under Windows PowerShell 5.1 or cmd.exe. The shell tool description already carries a shell-specific hint (shellSyntaxHint), but it sits far from the user turn and is drowned out by the agent's own system prompt; the env block sits at the top of every turn and is cached, so that is where the disambiguation belongs. Resolve the shell via the shared shellpath.DetectShell helper and print its base name plus the resolved path. Long-form dialect rules stay in the shell tool description (shellSyntaxHint) so this block stays terse and doesn't diverge from them. The base-name derivation lives in a new shellpath.ShellBaseName helper so the same logic isn't copied into hooks/builtins. No behaviour change on darwin/linux where the shell defaults to $SHELL or /bin/sh; the extra 'Shell:' line lands cleanly in the existing tab-indented env block.
130 lines
4.7 KiB
Go
130 lines
4.7 KiB
Go
// Package shellpath provides safe shell binary resolution to prevent
|
|
// PATH hijacking attacks (CWE-426).
|
|
package shellpath
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// ShellBaseName returns the lowercase shell name without extension. Splits on
|
|
// both separators so results are stable when the path came from another host OS.
|
|
func ShellBaseName(shellPath string) string {
|
|
base := shellPath
|
|
if i := strings.LastIndexAny(base, `/\`); i >= 0 {
|
|
base = base[i+1:]
|
|
}
|
|
return strings.ToLower(strings.TrimSuffix(base, filepath.Ext(base)))
|
|
}
|
|
|
|
// WindowsCmdExe returns the absolute path to cmd.exe on Windows using the
|
|
// SystemRoot environment variable (e.g. C:\Windows\System32\cmd.exe).
|
|
// This avoids resolving cmd.exe through PATH, which would be vulnerable
|
|
// to untrusted search path attacks (CWE-426).
|
|
//
|
|
// If the ComSpec environment variable is set, its value is returned as-is
|
|
// (ComSpec is typically set by Windows itself to the correct cmd.exe path).
|
|
//
|
|
// As a last resort, if neither ComSpec nor SystemRoot is set, it falls back
|
|
// to the bare "cmd.exe" name (should never happen on a normal Windows system).
|
|
func WindowsCmdExe() string {
|
|
if comspec := os.Getenv("ComSpec"); comspec != "" {
|
|
return comspec
|
|
}
|
|
if systemRoot := os.Getenv("SystemRoot"); systemRoot != "" {
|
|
return filepath.Join(systemRoot, "System32", "cmd.exe")
|
|
}
|
|
return "cmd.exe"
|
|
}
|
|
|
|
// DetectShell returns the appropriate shell binary and its argument prefix
|
|
// for the current platform.
|
|
//
|
|
// On Windows, it prefers PowerShell (pwsh.exe or powershell.exe) resolved
|
|
// via exec.LookPath (which returns an absolute path on success), falling
|
|
// back to cmd.exe resolved through [WindowsCmdExe].
|
|
//
|
|
// On Unix, it uses the SHELL environment variable or /bin/sh.
|
|
func DetectShell() (shell string, argsPrefix []string) {
|
|
if runtime.GOOS == "windows" {
|
|
return DetectWindowsShell()
|
|
}
|
|
|
|
return defaultUnixShell(), []string{"-c"}
|
|
}
|
|
|
|
// DetectWindowsShell returns the shell binary and argument prefix for Windows.
|
|
// It prefers PowerShell (resolved via LookPath, which returns an absolute path),
|
|
// falling back to cmd.exe via [WindowsCmdExe].
|
|
func DetectWindowsShell() (shell string, argsPrefix []string) {
|
|
if path, ok := lookPowerShell(); ok {
|
|
return path, []string{"-NoProfile", "-NonInteractive", "-Command"}
|
|
}
|
|
return WindowsCmdExe(), []string{"/C"}
|
|
}
|
|
|
|
// lookPowerShell resolves the preferred PowerShell binary (pwsh.exe first,
|
|
// then powershell.exe) via LookPath, which returns an absolute path.
|
|
func lookPowerShell() (string, bool) {
|
|
for _, ps := range []string{"pwsh.exe", "powershell.exe"} {
|
|
if path, err := exec.LookPath(ps); err == nil {
|
|
return path, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// DetectUnixShell returns the user's shell from the SHELL environment variable,
|
|
// falling back to /bin/sh.
|
|
func DetectUnixShell() string {
|
|
return defaultUnixShell()
|
|
}
|
|
|
|
// defaultUnixShell returns the user's shell from SHELL or /bin/sh.
|
|
func defaultUnixShell() string {
|
|
if shell := os.Getenv("SHELL"); shell != "" {
|
|
return shell
|
|
}
|
|
return "/bin/sh"
|
|
}
|
|
|
|
// InteractiveShellCmd returns a command that launches the user's preferred
|
|
// interactive shell, printing exitMsg first. The message is passed through an
|
|
// environment variable — never interpolated into the command line — so it
|
|
// cannot be reparsed as shell syntax. The command is owned by the caller
|
|
// (typically tea.ExecProcess), not by a request-scoped context, so
|
|
// exec.Command is intentional.
|
|
func InteractiveShellCmd(exitMsg string) *exec.Cmd {
|
|
const msgVar = "DOCKER_AGENT_SHELL_EXIT_MSG"
|
|
cmd := interactiveShellCmd(msgVar)
|
|
cmd.Env = append(os.Environ(), msgVar+"="+exitMsg)
|
|
return cmd
|
|
}
|
|
|
|
// interactiveShellCmd builds the platform-specific command that prints the
|
|
// message held in the msgVar environment variable, then hands over to an
|
|
// interactive shell.
|
|
func interactiveShellCmd(msgVar string) *exec.Cmd {
|
|
if runtime.GOOS != "windows" {
|
|
// printf (not `echo -e`) so the message prints verbatim under dash,
|
|
// bash, zsh and fish alike.
|
|
shell := DetectUnixShell()
|
|
return execCmd(shell, "-i", "-c", `printf '\n%s\n' "$`+msgVar+`"; exec `+shell)
|
|
}
|
|
if path, ok := lookPowerShell(); ok {
|
|
return execCmd(path, "-NoLogo", "-NoExit", "-Command", `Write-Host ""; Write-Host $env:`+msgVar)
|
|
}
|
|
// Absolute path to cmd.exe prevents PATH hijacking (CWE-426); /V:ON
|
|
// delayed expansion (!VAR!) keeps the message out of cmd's parser.
|
|
return execCmd(WindowsCmdExe(), "/V:ON", "/K", "echo. & echo !"+msgVar+"!")
|
|
}
|
|
|
|
// execCmd is a thin wrapper around exec.Command used for interactive
|
|
// processes whose lifecycle is owned by the caller (not a context).
|
|
func execCmd(name string, args ...string) *exec.Cmd {
|
|
return exec.Command(name, args...) //nolint:noctx // owned by the caller (tea.ExecProcess)
|
|
}
|