Commit b724e1c
authored
* fix: [#635] accept ${VAR} shell/dotenv syntax for env interpolation
Config substitution previously only accepted {env:VAR}. Users arriving
from Claude Code, VS Code, dotenv, or docker-compose naturally write
${VAR} and hit silent failures — the literal string passes through to
MCP servers as the env value, shadowing the forwarded parent env.
This adds ${VAR} as an alias for {env:VAR}. Regex matches POSIX
identifier names only ([A-Za-z_][A-Za-z0-9_]*) to avoid collisions
with random ${...} content in URLs or paths. Bare $VAR (without
braces) is intentionally NOT interpolated — too collision-prone.
- paths.ts: add second regex replace after the existing {env:VAR} pass
- paths-parsetext.test.ts: 6 new tests covering shell syntax, mixed
use, invalid identifier names, bare $VAR rejection, and MCP env
regression scenario
- mcp-servers.md: document both syntaxes with a table
Closes #635
* fix: address consensus review findings for PR #655
Applies fixes from multi-model consensus review (Claude + GPT 5.4 + Gemini 3.1 Pro).
- C1 (CRITICAL — JSON injection): ${VAR} substitution is now JSON-safe via
JSON.stringify(value).slice(1, -1). Env values with quotes/commas/newlines
can no longer break out of the enclosing JSON string. Existing {env:VAR}
keeps raw-injection semantics for backward compat (documented as the
opt-in power-user syntax).
- M2 (MAJOR — escape hatch): $${VAR} now preserves a literal ${VAR} in
the output (docker-compose convention). Negative lookbehind in the match
regex prevents substitution when preceded by $.
- m3 (MINOR — documentation): docs expanded with a 3-row syntax comparison
table explaining string-safe vs raw-injection modes.
- m4 (MINOR — tests): added 3 edge-case tests covering JSON injection
attempt, multiline/backslash values, and the new $${VAR} escape hatch.
- n5 (NIT — stale tip): TUI tip at tips.tsx:150 now mentions both
${VAR} and {env:VAR} syntaxes.
Deferred (larger refactor, scope discipline):
- M1 (comment handling): substitutions still run inside // comments. Same
pre-existing behavior for {env:VAR}. Would require unified substitution
pass. Can be a follow-up PR.
* feat: add ${VAR:-default} syntax for fallback values
Extends the ${VAR} substitution to support POSIX/docker-compose-style
defaults. Matches user expectations from dotenv, docker-compose, and
shell: default value is used when the variable is unset OR empty
(matching :- semantics rather than bare -).
- ${VAR:-default} uses 'default' when VAR is unset or empty string
- ${VAR:-} (empty default) resolves to empty string
- Defaults with spaces/special chars supported (${VAR:-Hello World})
- Default values are JSON-escaped (same security properties as ${VAR})
- $${VAR:-default} escape hatch works for literal preservation
Per research, ${VAR:-default} is the de facto standard across
docker-compose, dotenv, POSIX shell, GitHub Actions, and Terraform —
users arrive from these tools expecting this syntax.
Added 7 tests covering unset/set/empty cases, empty default, spaces,
JSON-injection attempt, and escape hatch.
* feat: add config_env_interpolation telemetry event
Collect signals on env-var interpolation usage to detect footguns and
guide future improvements. Tracks per config-load:
- dollar_refs: count of ${VAR} / ${VAR:-default} references
- dollar_unresolved: ${VAR} with no value and no default → empty string
(signal: users writing fragile configs without defaults)
- dollar_defaulted: ${VAR:-default} where the fallback was used
(signal: whether defaults syntax is actually being used)
- dollar_escaped: $${VAR} literal escapes used
- legacy_brace_refs: {env:VAR} references (raw-injection syntax)
- legacy_brace_unresolved: {env:VAR} with no value
Emitted via dynamic import to avoid circular dep with @/altimate/telemetry
(which imports @/config/config). Event fires only when interpolation
actually happens, so no-env-ref configs don't generate noise.
What this lets us answer after shipping:
- How many users hit 'unresolved' unintentionally? → consider failing loud
- Is ${VAR:-default} getting adopted? → iterate on docs if not
- Are users still writing the legacy {env:VAR}? → plan deprecation
- Is the $${VAR} escape rare? → simplify docs if so
Adds event type to ALL_EVENT_TYPES completeness list (43 → 44).
* fix: address P1 double-substitution from cubic/coderabbit review
Both bots flagged a backward-compat regression: ${VAR} pass runs after
{env:VAR} pass, so an env value containing literal ${X} got expanded in
the second pass. Reordering only fixed one direction (reverse cascade
still possible when ${VAR}'s output contains {env:Y}).
Correct fix: single-pass substitution with one regex alternation that
evaluates all three patterns against the ORIGINAL text. Output of any
pattern cannot be re-matched by another.
Single regex handles:
$${VAR} or $${VAR:-default} → literal escape
(?<!$)${VAR}[:-default] → JSON-safe substitution
{env:VAR} → raw text injection
Regression tests added for both cascade directions:
- env.A="${B}" + {env:A} → literal ${B} stays
- env.A="{env:B}" + ${A} → literal {env:B} stays
All 34 tests pass. Typecheck + marker guard clean.
1 parent 478d893 commit b724e1c
File tree
6 files changed
+304
-5
lines changed- docs/docs/configure
- packages/opencode
- src
- altimate/telemetry
- cli/cmd/tui/component
- config
- test
- config
- telemetry
6 files changed
+304
-5
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
23 | 38 | | |
24 | 39 | | |
25 | 40 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
659 | 659 | | |
660 | 660 | | |
661 | 661 | | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
662 | 681 | | |
663 | 682 | | |
664 | 683 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
150 | | - | |
| 150 | + | |
151 | 151 | | |
152 | 152 | | |
153 | 153 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
90 | | - | |
91 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
92 | 152 | | |
93 | 153 | | |
94 | 154 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
100 | 304 | | |
101 | 305 | | |
102 | 306 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
246 | 246 | | |
247 | 247 | | |
248 | 248 | | |
| 249 | + | |
249 | 250 | | |
250 | 251 | | |
251 | 252 | | |
252 | 253 | | |
253 | | - | |
| 254 | + | |
254 | 255 | | |
255 | 256 | | |
256 | 257 | | |
| |||
0 commit comments