docker-agent has two completely different env-variable expansion mechanisms, each used in a different subset of yaml fields. They use incompatible syntaxes (${env.X} vs $X / ${X}), neither errors when the wrong syntax appears in the wrong field, and there is no documentation that maps fields to the syntax they support. Users who infer the syntax from one example apply it to a similar-looking field and watch their config silently no-op.
The two mechanisms
JavaScript template literals via Goja
- Syntax:
${env.VAR} (JS member access on a dynamic env object), plus ${tool({...})} and arbitrary JS expressions.
- Used in:
agents.<name>.description, agents.<name>.welcome_message, agents.<name>.commands, toolsets[*].env values.
Shell-style env expansion via os.ExpandEnv
- Syntax:
$VAR or ${VAR} (POSIX-ish, no env. prefix), plus ~ for home directory.
- Used in:
toolsets[*].working_dir, toolset path resolution.
What this looks like to a user
| Field |
What works |
What silently fails |
agents.<name>.description |
${env.X} |
$X (literal) |
agents.<name>.welcome_message |
${env.X} |
$X (literal) |
agents.<name>.commands |
${env.X} |
$X (literal) |
agents.<name>.instruction |
(nothing — #2614) |
both |
toolsets[*].env (values) |
${env.X} |
$X (literal) |
toolsets[*].instruction |
(nothing — #2614) |
both |
toolsets[*].working_dir |
$X or ${X} |
${env.X} (literal as path component) |
| Toolset path refs |
$X or ${X} |
${env.X} (literal as path component) |
A user who learns from the description: ${env.HOME} example will reasonably write working_dir: ${env.HOME}/work and end up with a directory literally named ${env.HOME}. A user who learns from working_dir: $HOME/work will write description: $HOME and the model will see the literal $HOME.
Neither failure mode logs anything. The runtime accepts both yamls, loads the agent, and proceeds. A fast-fail at load (or a clear log line) would turn each instance into a 30-second fix. The current behavior turns it into a debugging session.
Suggestions
-
Unify on a single syntax. The JS template literal expander is the more powerful one (env access, tool calls, arbitrary expressions) and is already used in more places. Replace the os.ExpandEnv call sites in pkg/path/expand.go with the JS expander. Treat ~ as a separate path-only concern. This eliminates the dual-syntax problem entirely.
-
Make both syntaxes work everywhere. Run shell-style expansion as a pre-pass before the JS expander, or vice versa. Costs: more surface area, harder to reason about precedence, edge cases when a string legitimately contains $X. Possible but messy.
If none of those is acceptable, at minimum: add the matrix to the docs.
docker-agent has two completely different env-variable expansion mechanisms, each used in a different subset of yaml fields. They use incompatible syntaxes (
${env.X}vs$X/${X}), neither errors when the wrong syntax appears in the wrong field, and there is no documentation that maps fields to the syntax they support. Users who infer the syntax from one example apply it to a similar-looking field and watch their config silently no-op.The two mechanisms
JavaScript template literals via Goja
${env.VAR}(JS member access on a dynamicenvobject), plus${tool({...})}and arbitrary JS expressions.agents.<name>.description,agents.<name>.welcome_message,agents.<name>.commands,toolsets[*].envvalues.Shell-style env expansion via
os.ExpandEnv$VARor${VAR}(POSIX-ish, noenv.prefix), plus~for home directory.toolsets[*].working_dir, toolset path resolution.What this looks like to a user
agents.<name>.description${env.X}$X(literal)agents.<name>.welcome_message${env.X}$X(literal)agents.<name>.commands${env.X}$X(literal)agents.<name>.instructiontoolsets[*].env(values)${env.X}$X(literal)toolsets[*].instructiontoolsets[*].working_dir$Xor${X}${env.X}(literal as path component)$Xor${X}${env.X}(literal as path component)A user who learns from the
description: ${env.HOME}example will reasonably writeworking_dir: ${env.HOME}/workand end up with a directory literally named${env.HOME}. A user who learns fromworking_dir: $HOME/workwill writedescription: $HOMEand the model will see the literal$HOME.Neither failure mode logs anything. The runtime accepts both yamls, loads the agent, and proceeds. A fast-fail at load (or a clear log line) would turn each instance into a 30-second fix. The current behavior turns it into a debugging session.
Suggestions
Unify on a single syntax. The JS template literal expander is the more powerful one (env access, tool calls, arbitrary expressions) and is already used in more places. Replace the
os.ExpandEnvcall sites inpkg/path/expand.gowith the JS expander. Treat~as a separate path-only concern. This eliminates the dual-syntax problem entirely.Make both syntaxes work everywhere. Run shell-style expansion as a pre-pass before the JS expander, or vice versa. Costs: more surface area, harder to reason about precedence, edge cases when a string legitimately contains
$X. Possible but messy.If none of those is acceptable, at minimum: add the matrix to the docs.