mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
hooks/builtins: name the resolved shell in the env block
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.
This commit is contained in:
4 files changed
+49
-5
No files matched your search
@@ -6,13 +6,13 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/docker/docker-agent/pkg/hooks"
|
||||
"github.com/docker/docker-agent/pkg/shellpath"
|
||||
)
|
||||
|
||||
// AddEnvironmentInfo is the registered name of the add_environment_info builtin.
|
||||
const AddEnvironmentInfo = "add_environment_info"
|
||||
|
||||
// addEnvironmentInfo emits cwd / git / OS / arch info as session_start
|
||||
// additional context. No-op when Cwd is empty.
|
||||
// addEnvironmentInfo emits cwd/git/OS/arch/shell as session_start context.
|
||||
func addEnvironmentInfo(_ context.Context, in *hooks.Input, _ []string) (*hooks.Output, error) {
|
||||
if in == nil || in.Cwd == "" {
|
||||
return nil, nil
|
||||
@@ -20,20 +20,23 @@ func addEnvironmentInfo(_ context.Context, in *hooks.Input, _ []string) (*hooks.
|
||||
return hooks.NewAdditionalContextOutput(hooks.EventSessionStart, environmentInfo(in.Cwd)), nil
|
||||
}
|
||||
|
||||
// environmentInfo formats the env block injected at session_start:
|
||||
// working directory, git-repo status, and human-readable OS / arch.
|
||||
// environmentInfo builds the <env> block. Long-form dialect rules live in
|
||||
// shellSyntaxHint (tool description) and shellDialectHint (reactive) so this
|
||||
// stays terse.
|
||||
func environmentInfo(workingDir string) string {
|
||||
gitRepo := "No"
|
||||
if isGitRepo(workingDir) {
|
||||
gitRepo = "Yes"
|
||||
}
|
||||
shellPath, _ := shellpath.DetectShell()
|
||||
return fmt.Sprintf(`Here is useful information about the environment you are running in:
|
||||
<env>
|
||||
Working directory: %s
|
||||
Is directory a git repo: %s
|
||||
Operating System: %s
|
||||
CPU Architecture: %s
|
||||
</env>`, workingDir, gitRepo, displayOS(), displayArch())
|
||||
Shell: %s (%s)
|
||||
</env>`, workingDir, gitRepo, displayOS(), displayArch(), shellpath.ShellBaseName(shellPath), shellPath)
|
||||
}
|
||||
|
||||
// displayOS returns a friendlier label for the common values of
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/docker/docker-agent/pkg/shellpath"
|
||||
)
|
||||
|
||||
func TestEnvironmentInfo(t *testing.T) {
|
||||
@@ -49,12 +51,14 @@ func TestEnvironmentInfo(t *testing.T) {
|
||||
if tt.expectGit {
|
||||
gitStatus = "Yes"
|
||||
}
|
||||
shellPath, _ := shellpath.DetectShell()
|
||||
expected := `Here is useful information about the environment you are running in:
|
||||
<env>
|
||||
Working directory: ` + dir + `
|
||||
Is directory a git repo: ` + gitStatus + `
|
||||
Operating System: ` + displayOS() + `
|
||||
CPU Architecture: ` + displayArch() + `
|
||||
Shell: ` + shellpath.ShellBaseName(shellPath) + ` (` + shellPath + `)
|
||||
</env>`
|
||||
|
||||
assert.Equal(t, expected, environmentInfo(dir))
|
||||
|
||||
@@ -7,8 +7,19 @@ import (
|
||||
"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
|
||||
|
||||
@@ -7,6 +7,32 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestShellBaseName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{path: "/bin/zsh", expected: "zsh"},
|
||||
{path: "/usr/local/bin/fish", expected: "fish"},
|
||||
{path: "/bin/sh", expected: "sh"},
|
||||
{path: `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`, expected: "powershell"},
|
||||
{path: `C:\Program Files\PowerShell\7\pwsh.exe`, expected: "pwsh"},
|
||||
{path: `C:\Windows\System32\cmd.exe`, expected: "cmd"},
|
||||
{path: "", expected: ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := ShellBaseName(tt.path); got != tt.expected {
|
||||
t.Errorf("ShellBaseName(%q) = %q, want %q", tt.path, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsCmdExe_ComSpec(t *testing.T) {
|
||||
t.Setenv("ComSpec", `C:\Custom\cmd.exe`)
|
||||
got := WindowsCmdExe()
|
||||
|
||||
Reference in new issue
Block a user