Skip to content

Commit e1f7da9

Browse files
authored
Merge pull request #607 from smazmi/feat/604-support-env-placeholders-in-agent-commands
feat: add environment variable placeholder expansion for agent commands (#604)
2 parents fb4fdc9 + 0bdcf45 commit e1f7da9

5 files changed

Lines changed: 144 additions & 18 deletions

File tree

docs/USAGE.md

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -114,31 +114,28 @@ agents:
114114
commands: # Either mapping or list of singleton maps
115115
df: "check how much free space i have on my disk"
116116
ls: "list the files in the current directory"
117+
greet: "Say hello to $USER and ask how their day is going"
118+
analyze: "Analyze the project named $PROJECT_NAME in the $ENVIRONMENT environment"
117119
```
118120
119-
### Running with named commands
120-
121-
- Example YAML forms supported:
122-
123-
```yaml
124-
commands:
125-
df: "check how much free space i have on my disk"
126-
ls: "list the files in the current directory"
127-
```
128-
129-
```yaml
130-
commands:
131-
- df: "check how much free space i have on my disk"
132-
- ls: "list the files in the current directory"
133-
```
134-
135-
Run:
121+
### Running named commands
136122
137123
```bash
138124
cagent run ./agent.yaml /df
139125
cagent run ./agent.yaml /ls
126+
127+
export USER=alice
128+
cagent run ./agent.yaml /greet
129+
130+
export PROJECT_NAME=myproject
131+
export ENVIRONMENT=production
132+
cagent run ./agent.yaml /analyze
140133
```
141134

135+
- Placeholders are expanded during agent loading using available environment variables
136+
- Undefined variables expand to empty strings (no error is thrown)
137+
- Both `$VAR` and `${VAR}` syntax are supported
138+
142139
### Model Properties
143140

144141
| Property | Type | Description | Required |

examples/env_placeholders.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env cagent run
2+
version: "2"
3+
4+
# Example demonstrating environment variable placeholder expansion in commands
5+
#
6+
# This example shows how to use $VARIABLE_NAME or ${VARIABLE_NAME} placeholders
7+
# in command definitions, which are automatically replaced with environment variable
8+
# values when the agent is loaded.
9+
#
10+
# Usage:
11+
# export USER=alice
12+
# export PROJECT_NAME=myproject
13+
# export ENVIRONMENT=production
14+
#
15+
# cagent run env_placeholders.yaml -c greet
16+
# cagent run env_placeholders.yaml -c analyze
17+
# cagent run env_placeholders.yaml -c status
18+
#
19+
# Note: Undefined environment variables expand to empty strings without causing errors,
20+
# allowing you to only define the variables needed for the commands you actually use.
21+
22+
agents:
23+
root:
24+
model: anthropic/claude-sonnet-4-0
25+
description: "Agent demonstrating environment variable placeholder expansion"
26+
instruction: |
27+
You are a helpful assistant that responds to user requests.
28+
Provide clear, concise, and actionable responses.
29+
30+
commands:
31+
greet: "Say hello to $USER and ask how their day is going"
32+
analyze: "Analyze the project named $PROJECT_NAME in the $ENVIRONMENT environment and provide insights"
33+
status: "Check the status of the service ${SERVICE_NAME} and report any issues"
34+
report: "Generate a comprehensive report for project $PROJECT_NAME owned by $OWNER deployed in $LOCATION"
35+
inspect: "Inspect the contents of the directory at $WORKING_DIR and summarize what you find"
36+
help: "Explain what you can do and how to use environment variable placeholders in commands"
37+
38+
toolsets:
39+
- type: filesystem
40+
- type: shell

pkg/environment/expand.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ func Expand(ctx context.Context, value string, env Provider) (string, error) {
3939
return expanded, nil
4040
}
4141

42+
// ExpandLenient expands environment variables without returning errors for undefined variables.
43+
// Undefined variables expand to empty strings, similar to standard shell behavior.
44+
func ExpandLenient(ctx context.Context, value string, env Provider) string {
45+
return os.Expand(value, func(name string) string {
46+
return env.Get(ctx, name)
47+
})
48+
}
49+
4250
func ToValues(envMap map[string]string) []string {
4351
var values []string
4452
for k, v := range envMap {

pkg/teamloader/teamloader.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ func createThinkTool(ctx context.Context, toolset latest.Toolset, parentDir stri
111111
return builtin.NewThinkTool(), nil
112112
}
113113

114+
// expandCommandPlaceholders expands environment variable placeholders in command values.
115+
// Undefined variables are allowed and will expand to empty strings, enabling users to
116+
// only define the environment variables needed for the commands they actually use.
117+
func expandCommandPlaceholders(ctx context.Context, commands map[string]string, envProvider environment.Provider) map[string]string {
118+
expanded := map[string]string{}
119+
120+
for name, value := range commands {
121+
expanded[name] = environment.ExpandLenient(ctx, value, envProvider)
122+
}
123+
124+
return expanded
125+
}
126+
114127
func createShellTool(ctx context.Context, toolset latest.Toolset, parentDir string, envProvider environment.Provider, runtimeConfig config.RuntimeConfig) (tools.ToolSet, error) {
115128
env, err := environment.ExpandAll(ctx, environment.ToValues(toolset.Env), envProvider)
116129
if err != nil {
@@ -343,7 +356,7 @@ func Load(ctx context.Context, p string, runtimeConfig config.RuntimeConfig, opt
343356
agent.WithAddPromptFiles(agentConfig.AddPromptFiles),
344357
agent.WithMaxIterations(agentConfig.MaxIterations),
345358
agent.WithNumHistoryItems(agentConfig.NumHistoryItems),
346-
agent.WithCommands(agentConfig.Commands),
359+
agent.WithCommands(expandCommandPlaceholders(ctx, agentConfig.Commands, env)),
347360
}
348361

349362
models, err := getModelsForAgent(ctx, cfg, &agentConfig, env, runtimeConfig)

pkg/teamloader/teamloader_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,71 @@ func TestToolsetInstructions(t *testing.T) {
143143
expected := "Dummy fetch tool instruction"
144144
require.Equal(t, expected, instructions)
145145
}
146+
147+
// TestExpandCommandPlaceholders tests that $placeholders in commands are expanded with env var values
148+
func TestExpandCommandPlaceholders(t *testing.T) {
149+
tests := []struct {
150+
name string
151+
commands map[string]string
152+
envVars map[string]string
153+
expected map[string]string
154+
}{
155+
{
156+
name: "single placeholder",
157+
commands: map[string]string{"greet": "Say hello to $USER"},
158+
envVars: map[string]string{"USER": "alice"},
159+
expected: map[string]string{"greet": "Say hello to alice"},
160+
},
161+
{
162+
name: "multiple placeholders",
163+
commands: map[string]string{"analyze": "Analyze $PROJECT_NAME in $ENVIRONMENT"},
164+
envVars: map[string]string{"PROJECT_NAME": "myproject", "ENVIRONMENT": "production"},
165+
expected: map[string]string{"analyze": "Analyze myproject in production"},
166+
},
167+
{
168+
name: "no placeholders",
169+
commands: map[string]string{"simple": "List all files"},
170+
envVars: map[string]string{},
171+
expected: map[string]string{"simple": "List all files"},
172+
},
173+
{
174+
name: "placeholder with curly braces",
175+
commands: map[string]string{"check": "Check ${SERVICE_NAME} status"},
176+
envVars: map[string]string{"SERVICE_NAME": "api-server"},
177+
expected: map[string]string{"check": "Check api-server status"},
178+
},
179+
{
180+
name: "missing env var expands to empty string",
181+
commands: map[string]string{"test": "Check $MISSING_VAR status"},
182+
envVars: map[string]string{},
183+
expected: map[string]string{"test": "Check status"},
184+
},
185+
{
186+
name: "empty commands",
187+
commands: map[string]string{},
188+
envVars: map[string]string{},
189+
expected: map[string]string{},
190+
},
191+
}
192+
193+
for _, tt := range tests {
194+
t.Run(tt.name, func(t *testing.T) {
195+
// Create a test environment provider
196+
env := &testEnvProvider{vars: tt.envVars}
197+
198+
// Expand the commands
199+
result := expandCommandPlaceholders(t.Context(), tt.commands, env)
200+
201+
require.Equal(t, tt.expected, result)
202+
})
203+
}
204+
}
205+
206+
// testEnvProvider is a simple environment provider for testing
207+
type testEnvProvider struct {
208+
vars map[string]string
209+
}
210+
211+
func (p *testEnvProvider) Get(_ context.Context, name string) string {
212+
return p.vars[name]
213+
}

0 commit comments

Comments
 (0)