@@ -5,7 +5,6 @@ import (
10 "net/http"
9 "net/http"
@@ -301,25 +300,46 @@ func (l LSPs) Sorted() []LSP {
301 return sorted
300 return sorted
304−func (l LSPConfig) ResolvedEnv() []string {
303+// ResolvedEnv returns m.Env with every value expanded through the
305− return resolveEnvs(l.Env)
304+// shell resolver. The returned slice is of the form "KEY=value" sorted
306−}
305+// by key so callers get deterministic output; the receiver's Env map is
307−
306+// not mutated. On the first resolution failure it returns nil and an
307+// error that identifies the offending key; the inner resolver error is
308+// already sanitized by ResolveValue and is wrapped with %w so
309+// errors.Is/As continues to work. Callers are expected to surface it
310+// (for MCP, via StateError on the status card) rather than silently
311+// spawn the server with an empty credential.
308−func (m MCPConfig) ResolvedEnv() []string {
312+func (m MCPConfig) ResolvedEnv() ([]string, error) {
309 return resolveEnvs(m.Env)
313 return resolveEnvs(m.Env)
316+// ResolvedHeaders returns m.Headers with every value expanded through
317+// the shell resolver. A fresh map is allocated; m.Headers is never
318+// mutated. On the first resolution failure it returns nil and an error
319+// identifying the offending header name; the inner resolver error is
320+// already sanitized by ResolveValue and is wrapped with %w so
321+// errors.Is/As continues to work.
312−func (m MCPConfig) ResolvedHeaders() map[string]string {
322+func (m MCPConfig) ResolvedHeaders() (map[string]string, error) {
323+ if len(m.Headers) == 0 {
324+ return map[string]string{}, nil
313 resolver := NewShellVariableResolver(env.New())
326 resolver := NewShellVariableResolver(env.New())
314− for e, v := range m.Headers {
327+ out := make(map[string]string, len(m.Headers))
315− var err error
328+ // Sort keys so failures are reported deterministically when more
329+ // than one header would fail.
330+ keys := make([]string, 0, len(m.Headers))
331+ for k := range m.Headers {
332+ keys = append(keys, k)
335+ for _, k := range keys {
316− m.Headers[e], err = resolver.ResolveValue(v)
336+ v, err := resolver.ResolveValue(m.Headers[k])
317 if err != nil {
337 if err != nil {
318− slog.Error("Error resolving header variable", "error", err, "variable", e, "value", v)
338+ return nil, fmt.Errorf("header %s: %w", k, err)
322− return m.Headers
342+ return out, nil
325type Agent struct {
345type Agent struct {
@@ -659,22 +679,30 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
659 return nil
679 return nil
682+// resolveEnvs expands every value in envs through the shell resolver
683+// and returns a fresh "KEY=value" slice sorted by key. The input map is
684+// not mutated. On the first resolution failure it returns nil and an
685+// error identifying the offending variable; the inner resolver error is
686+// already sanitized by ResolveValue and is wrapped with %w.
662−func resolveEnvs(envs map[string]string) []string {
687+func resolveEnvs(envs map[string]string) ([]string, error) {
663 resolver := NewShellVariableResolver(env.New())
691 resolver := NewShellVariableResolver(env.New())
664− for e, v := range envs {
692+ keys := make([]string, 0, len(envs))
665− var err error
693+ for k := range envs {
666− envs[e], err = resolver.ResolveValue(v)
694+ keys = append(keys, k)
668− slog.Error("Error resolving environment variable", "error", err, "variable", e, "value", v)
672−
696+ slices.Sort(keys)
673 res := make([]string, 0, len(envs))
697 res := make([]string, 0, len(envs))
674− for k, v := range envs {
698+ for _, k := range keys {
699+ v, err := resolver.ResolveValue(envs[k])
701+ return nil, fmt.Errorf("env %s: %w", k, err)
675 res = append(res, fmt.Sprintf("%s=%s", k, v))
703 res = append(res, fmt.Sprintf("%s=%s", k, v))
677− return res
705+ return res, nil
680func ptrValOr[T any](t *T, el T) T {
708func ptrValOr[T any](t *T, el T) T {