Skip to content

Conversation

@smazmi
Copy link
Contributor

@smazmi smazmi commented Oct 24, 2025

Description

Implements support for $placeholder syntax in agent commands defined in agent.yaml files. When a command is invoked using the -c/--command flag, any $VARIABLE_NAME or ${VARIABLE_NAME} placeholders are automatically replaced with their corresponding environment variable values.

Fixes #604

Changes

  • Modified cmd/root/run.go:
    • Added import for pkg/environment package
    • Added placeholder expansion logic after command lookup (lines 246-251)
    • Uses environment.NewDefaultProvider() to access env vars from multiple sources (OS env, secrets, keychain, etc.)
    • Returns clear error message if a required environment variable is not set

Implementation Details

The expansion leverages existing infrastructure:

  • environment.Expand() - Handles both $VAR and ${VAR} syntax
  • environment.NewDefaultProvider() - Provides access to multiple environment sources

Maintains full backward compatibility - commands without placeholders work exactly as before.

Example Usage

Before:

$ cagent run agent.yaml -c greet
# Agent receives literal: "Say hello to $USER"

After:

$ export USER=alice
$ cagent run agent.yaml -c greet
# Agent receives expanded: "Say hello to alice"

Agent configuration example:

agents:
  root:
    model: anthropic/claude-sonnet-4-0
    commands:
      greet: "Say hello to $USER and ask how their day is going"
      analyze: "Analyze the project named $PROJECT_NAME"
      path: "List contents of ${WORKING_DIR}"

Testing

Tested with Docker Model Runner (DMR) using ai/gpt-oss:latest

Manual Testing

Tested with multiple scenarios:

  • Single placeholder expansion ($USER)
  • Multiple placeholders in one command ($PROJECT_NAME, $OWNER, $LOCATION)
  • Both $VAR and ${VAR} syntax
  • Commands without placeholders (backward compatibility)
  • Error handling when environment variable is missing

Checklist

  • Code follows project style and patterns
  • Builds successfully (task build)
  • No lint errors (task lint)
  • Manually tested multiple scenarios
  • Backward compatibility maintained
  • Clear error messages for missing variables
  • Uses existing environment package infrastructure

Signed-off-by: Sadique Azmi [email protected]

@smazmi smazmi requested a review from a team as a code owner October 24, 2025 20:51
@smazmi
Copy link
Contributor Author

smazmi commented Oct 24, 2025

I've tested this thoroughly with different placeholder scenarios and it all works well! I have some example YAML files I created during development - would you like me to include those in this PR at like examples/ or keep it minimal? Happy either way! 😊

@rumpl
Copy link
Member

rumpl commented Oct 24, 2025

Thank you!

Adding examples would be great yes. And if you have the time, a couple of lines of documentation in docs/usage.md would be amazing.

cmd/root/run.go Outdated
if msg, ok := cmds[trimmed]; ok {
commandFirstMessage = &msg
// Expand $placeholders with environment variable values
env := environment.NewDefaultProvider()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commenting from my phone, I think this code should be in the teamloader, that way all the other commands (exec, api…) can have the same behavior

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! That makes sense, I'll take a look.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick question about the current behavior right now, undefined variables cause an error. This means if a user runs -c <some_command> without defining a variable used in that command, it fails. However, if I move the expansion logic to the teamloader, it attempts to expand all variables during agent loading and fails if any variable is undefined.

I’m considering introducing a more lenient expand function that still allows the agent to load even if some variables are undefined in that case, those variables would just be replaced with empty strings instead of causing an error. Does that sound right?

Copy link
Member

@rumpl rumpl Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to not fail but replace with empty, after all echo $SOME_VAR_THAT_DOESNT_EXIST doesn't fail but just returns an empty string.

In the future we might add the same interpolation logic as compose has but we can do that in a followup :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after all echo $SOME_VAR_THAT_DOESNT_EXIST doesn't fail but just returns an empty string.

Yeah, that’s what I was thinking too, makes perfect sense. I’ll get it done then, glad I asked! Thanks :)

@smazmi
Copy link
Contributor Author

smazmi commented Oct 24, 2025

Thank you!

Adding examples would be great yes. And if you have the time, a couple of lines of documentation in docs/usage.md would be amazing.

Absolutely! I’ll get it done

agentsByName := make(map[string]*agent.Agent)

for name, agentConfig := range cfg.Agents {
// Expand environment variable placeholders in commands
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the expansion happen just when the command is about to run?
If that's too complicated, that's ok. I just want things to be as lazy as possible.

Copy link
Contributor Author

@smazmi smazmi Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @dgageot! I appreciate the feedback about lazy evaluation.

Quick question: if we do lazy expansion, doesn't that essentially mean we're back to my original implementation - expanding in run.go when the command is used?

The only difference would be calling a teamloader helper function instead of environment.Expand() directly. @rumpl suggested keeping expansion logic in teamloader for architectural consistency, but for lazy expansion, the actual call would happen in run.go.

So is this the right direction - keep the expansion utility in teamloader but call it lazily from run.go when the command is actually used?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll sync with @rumpl and will come back to you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small update, we didn't have the time to sync today sorry, we'll get to it as soon as possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, No problem

smazmi and others added 2 commits October 30, 2025 10:19
Implement support for $placeholder syntax in commands defined in agent.yaml
files. When a command is invoked using the -c/--command flag, any
$VARIABLE_NAME or ${VARIABLE_NAME} placeholders in the command string are
now automatically replaced with their corresponding environment variable
values.

The expansion uses the existing
environment.Expand() function and environment.NewDefaultProvider() to access
environment variables from multiple sources (OS env, secrets, keychain, etc.).

Implementation details:
- Added placeholder expansion in cmd/root/run.go after command lookup
- Uses environment.Expand() for consistent variable resolution
- Returns clear error messages when required environment variables are missing
- Maintains backward compatibility with commands that don't use placeholders

Fixes docker#604

Signed-off-by: Sadique Azmi <[email protected]>
Signed-off-by: David Gageot <[email protected]>
@dgageot dgageot force-pushed the feat/604-support-env-placeholders-in-agent-commands branch from 5c5b510 to 0bdcf45 Compare October 30, 2025 09:27
@dgageot dgageot merged commit e1f7da9 into docker:main Oct 30, 2025
5 checks passed
@smazmi
Copy link
Contributor Author

smazmi commented Oct 30, 2025

Thanks!

@smazmi smazmi deleted the feat/604-support-env-placeholders-in-agent-commands branch October 30, 2025 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

commands support $ placeholders

3 participants