Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/configuration/agents/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ agents:
| `toolsets` | array | ✗ | List of tool configurations. See [Tool Config](../tools/index.md). |
| `fallback` | object | ✗ | Automatic model failover configuration. |
| `add_date` | boolean | ✗ | When `true`, injects the current date into the agent's context. |
| `add_environment_info` | boolean | ✗ | When `true`, injects working directory, OS, CPU architecture, and git info into context. |
| `add_environment_info` | boolean | ✗ | When `true`, injects working directory, OS, CPU architecture, git info, and the resolved shell into context. |
| `add_prompt_files` | array | ✗ | List of file paths whose contents are appended to the system prompt. Useful for including coding standards, guidelines, or additional context. |
| `add_description_parameter` | boolean | ✗ | When `true`, adds agent descriptions as a parameter in tool schemas. Helps with tool selection in multi-agent scenarios. |
| `redact_secrets` | boolean | ✗ | When `true`, scrubs detected secrets (API keys, tokens, private keys, etc.) out of tool-call arguments, outgoing chat messages, and tool output before they reach a tool, the model, or downstream consumers. See [Redacting Secrets](#redacting-secrets) below. |
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/hooks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ Built-ins are typically zero-config and faster than equivalent shell hooks becau
| Builtin | Event | Args | What it does |
| ----------------------- | ----------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `add_date` | `turn_start` | _none_ | Prepends `Today's date: YYYY-MM-DD` so the model always knows the current date. |
| `add_environment_info` | `session_start` | _none_ | Adds the working directory, git-repo status, OS, and CPU architecture. |
| `add_environment_info` | `session_start` | _none_ | Adds the working directory, git-repo status, OS, CPU architecture, and the resolved shell. |
| `add_prompt_files` | `turn_start` | `[file1, file2, ...]` | Reads each named file from the workdir hierarchy (walking up) and the home directory, and appends their contents. |
| `add_git_status` | `turn_start` | _none_ | Adds the output of `git status --short --branch` (no-op outside a git repo or when git isn't installed). |
| `add_git_diff` | `turn_start` | _none_, or `["full"]` | Adds `git diff --stat` by default. Pass `args: ["full"]` to emit the full unified diff. Output is capped to 4 KB. |
Expand Down
13 changes: 8 additions & 5 deletions pkg/hooks/builtins/add_environment_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,37 @@ 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.
Comment thread
trungutt marked this conversation as resolved.
// No-op when Cwd is empty.
func addEnvironmentInfo(_ context.Context, in *hooks.Input, _ []string) (*hooks.Output, error) {
if in == nil || in.Cwd == "" {
return nil, nil
}
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) so this stays terse.
func environmentInfo(workingDir string) string {
gitRepo := "No"
if isGitRepo(workingDir) {
gitRepo = "Yes"
}
shellPath, _ := shellpath.DetectShell() // second value is argsPrefix, unused here
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
Expand Down
4 changes: 4 additions & 0 deletions pkg/hooks/builtins/add_environment_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
Expand Down
11 changes: 11 additions & 0 deletions pkg/shellpath/shellpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
trungutt marked this conversation as resolved.
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
Expand Down
26 changes: 26 additions & 0 deletions pkg/shellpath/shellpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
16 changes: 2 additions & 14 deletions pkg/tools/builtin/shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func (t *ToolSet) Instructions() string {
- Use "cwd" parameter instead of cd within commands
- Combine operations with pipes, redirections, and heredocs
- Non-zero exit codes return error info with output; timed-out commands are terminated`,
shellBaseName(t.handler.shell), displayOS())
shellpath.ShellBaseName(t.handler.shell), displayOS())
}

func (t *ToolSet) Tools(context.Context) ([]tools.Tool, error) {
Expand Down Expand Up @@ -409,7 +409,7 @@ func (t *ToolSet) Stop(context.Context) error {
// resolved shell is PowerShell or cmd.exe (e.g. "pwd && ls -la" is a
// parse error under Windows PowerShell 5.1).
func shellToolDescription(shellPath string) string {
name := shellBaseName(shellPath)
name := shellpath.ShellBaseName(shellPath)
desc := fmt.Sprintf("Executes the given shell command with %s on %s.", name, displayOS())
if hint := shellSyntaxHint(name); hint != "" {
desc += " " + hint
Expand All @@ -435,18 +435,6 @@ func shellSyntaxHint(name string) string {
}
}

// shellBaseName reduces a resolved shell path to a lowercase name the
// model can recognize (C:\...\powershell.exe -> powershell, /bin/zsh -> zsh).
// Splits on both separators instead of filepath.Base so the result is
// deterministic regardless of the host OS the path came from.
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)))
}

// displayOS returns a friendlier label for the common values of
// runtime.GOOS, falling back to GOOS itself for anything exotic.
func displayOS() string {
Expand Down
27 changes: 3 additions & 24 deletions pkg/tools/builtin/shell/shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/docker/docker-agent/pkg/config"
"github.com/docker/docker-agent/pkg/shellpath"
"github.com/docker/docker-agent/pkg/tools"
)

Expand Down Expand Up @@ -200,7 +201,7 @@ func TestShellTool_Instructions(t *testing.T) {
instructions := tool.Instructions()

assert.Contains(t, instructions, "Shell Tools")
assert.Contains(t, instructions, shellBaseName(tool.handler.shell),
assert.Contains(t, instructions, shellpath.ShellBaseName(tool.handler.shell),
"instructions must name the resolved shell so the model uses its syntax")
assert.Contains(t, instructions, displayOS())
assert.NotContains(t, instructions, "run_background_job")
Expand All @@ -219,32 +220,10 @@ func TestShellTool_DescriptionNamesInterpreter(t *testing.T) {
require.Len(t, allTools, 1)

description := allTools[0].Description
assert.Contains(t, description, shellBaseName(tool.handler.shell))
assert.Contains(t, description, shellpath.ShellBaseName(tool.handler.shell))
assert.Contains(t, description, displayOS())
}

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: `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"},
}

for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expected, shellBaseName(tt.path))
})
}
}

func TestResolveWorkDir(t *testing.T) {
t.Parallel()

Expand Down
Loading