Skip to content

Resolve environment variable placeholders in configuration#47

Open
iHildy wants to merge 4 commits intomainfrom
fix/mcp-env-placeholders-resolution-17967297301129502178
Open

Resolve environment variable placeholders in configuration#47
iHildy wants to merge 4 commits intomainfrom
fix/mcp-env-placeholders-resolution-17967297301129502178

Conversation

@iHildy
Copy link
Owner

@iHildy iHildy commented Mar 14, 2026

Implemented environment variable placeholder resolution in the configuration system. This ensures that {env:VAR_NAME} placeholders (typically used for MCP secrets) are replaced with actual environment variable values at runtime. Added a recursive resolveEnvPlaceholders function and integrated it into applyOverridesToRuntimeConfig. Included comprehensive tests for various resolution scenarios.

Fixes #44


PR created automatically by Jules for task 17967297301129502178 started by @iHildy

MCP secrets are often scrubbed and replaced with {env:VAR} placeholders
by the opencode-synced plugin. However, these placeholders were not
being resolved by opencode at runtime, causing authentication failures.

This change implements a recursive environment variable resolution
mechanism in `applyOverridesToRuntimeConfig` to ensure that these
placeholders are correctly replaced with values from `process.env`.

Added tests to verify resolution of nested placeholders and handling
of missing environment variables.

Co-authored-by: iHildy <25069719+iHildy@users.noreply.github.com>
@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's configuration system by enabling the dynamic resolution of environment variable placeholders. This change allows sensitive information, such as secrets, to be injected into the configuration at runtime via environment variables, improving security practices and making the application more adaptable across different deployment environments without hardcoding values.

Highlights

  • Environment Variable Resolution: Introduced a new mechanism to resolve {env:VAR_NAME} placeholders within the application's configuration, replacing them with actual environment variable values at runtime.
  • New resolveEnvPlaceholders Function: Added a recursive resolveEnvPlaceholders function to traverse configuration objects, arrays, and strings, applying the environment variable resolution logic.
  • Configuration Integration: Integrated the new resolution logic into applyOverridesToRuntimeConfig to ensure that environment variables are resolved after configuration overrides are applied.
  • Comprehensive Testing: Included a new test file (src/sync/resolve.test.ts) with comprehensive tests covering successful resolution and handling of missing environment variables.
Changelog
  • src/sync/config.ts
    • Introduced resolveEnvPlaceholders function for recursive environment variable resolution.
    • Modified applyOverridesToRuntimeConfig to call resolveEnvPlaceholders on the merged configuration.
  • src/sync/resolve.test.ts
    • Added a new test file to validate the correct behavior of environment variable resolution, including cases where variables are present and absent.
Activity
  • The pull request was created automatically by Jules for task 17967297301129502178, started by @iHildy. No other human activity has been recorded yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

google-labs-jules bot and others added 2 commits March 14, 2026 22:01
MCP secrets are often scrubbed and replaced with {env:VAR} placeholders
by the opencode-synced plugin. However, these placeholders were not
being resolved by opencode at runtime, causing authentication failures.

This change implements a recursive environment variable resolution
mechanism in `applyOverridesToRuntimeConfig` to ensure that these
placeholders are correctly replaced with values from `process.env`.

Added tests to verify resolution of nested placeholders and handling
of missing environment variables.

Co-authored-by: iHildy <25069719+iHildy@users.noreply.github.com>
MCP secrets are often scrubbed and replaced with {env:VAR} placeholders
by the opencode-synced plugin. However, these placeholders were not
being resolved by opencode at runtime, causing authentication failures.

This change implements a recursive environment variable resolution
mechanism in `applyOverridesToRuntimeConfig` to ensure that these
placeholders are correctly replaced with values from `process.env`.

Added tests to verify resolution of nested placeholders and handling
of missing environment variables.

Co-authored-by: iHildy <25069719+iHildy@users.noreply.github.com>
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a mechanism to resolve environment variable placeholders in the configuration. The implementation adds a recursive resolveEnvPlaceholders function. My review found a critical issue in the regular expression used for placeholder matching which would cause a runtime error. I've also suggested an improvement to make the recursive function robust against circular references. Additionally, I've provided feedback on the new tests to improve their isolation and reliability by using Vitest's environment stubbing features instead of modifying process.env directly.

Comment on lines +195 to 216
export function resolveEnvPlaceholders(config: unknown): unknown {
if (typeof config === 'string') {
return config.replace(/\{env:([^}]+)\}/g, (match, envVar) => {
const value = process.env[envVar];
return value !== undefined ? value : match;
});
}

if (Array.isArray(config)) {
return config.map((item) => resolveEnvPlaceholders(item));
}

if (isPlainObject(config)) {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(config)) {
result[key] = resolveEnvPlaceholders(value);
}
return result;
}

return config;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

This function has a critical bug and a potential robustness issue.

  1. Critical Bug: The regular expression /{env:([^}]+)}/g is invalid because the { character is not escaped. This will cause a SyntaxError at runtime. It must be escaped as \{.
  2. Robustness: The function is recursive and does not handle circular references in the input object, which could lead to a stack overflow.

I've provided a suggestion that fixes the regex and adds cycle detection using a WeakSet.

export function resolveEnvPlaceholders(config: unknown, seen = new WeakSet<object>()): unknown {
  if (typeof config === 'string') {
    return config.replace(/\{env:([^}]+)\}/g, (match, envVar) => {
      const value = process.env[envVar];
      return value !== undefined ? value : match;
    });
  }

  if (config === null || typeof config !== 'object') {
    return config;
  }

  if (seen.has(config)) {
    return config;
  }
  seen.add(config);

  if (Array.isArray(config)) {
    return config.map((item) => resolveEnvPlaceholders(item, seen));
  }

  if (isPlainObject(config)) {
    const result: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(config)) {
      result[key] = resolveEnvPlaceholders(value, seen);
    }
    return result;
  }

  return config;
}

Comment on lines +4 to +61
describe('applyOverridesToRuntimeConfig environment resolution', () => {
it('resolves {env:VAR} placeholders from process.env', () => {
process.env.TEST_VAR = 'secret-value';
process.env.OTHER_VAR = 'other-value';

const config: Record<string, unknown> = {
mcp: {
github: {
headers: {
Authorization: 'Bearer {env:TEST_VAR}',
},
other: '{env:OTHER_VAR}',
},
},
unrelated: 'keep-me',
};

const overrides = {
mcp: {
github: {
headers: {
Authorization: 'Bearer {env:TEST_VAR}',
},
},
},
};

// We apply overrides (which might already contain placeholders)
applyOverridesToRuntimeConfig(config, overrides);

const mcp = config.mcp as Record<string, Record<string, Record<string, string>>>;
expect(mcp.github.headers.Authorization).toBe('Bearer secret-value');
expect(mcp.github.other).toBe('other-value');
expect(config.unrelated).toBe('keep-me');

delete process.env.TEST_VAR;
delete process.env.OTHER_VAR;
});

it('handles missing environment variables by leaving placeholder intact', () => {
process.env.PRESENT_VAR = 'present';
delete process.env.ABSENT_VAR;

const config: Record<string, unknown> = {
val: '{env:PRESENT_VAR}',
missing: '{env:ABSENT_VAR}',
};

applyOverridesToRuntimeConfig(config, {});

expect(config.val).toBe('present');
// If it's missing, we keep it as is to avoid breaking things silently or passing empty strings
// that might be harder to debug.
expect(config.missing).toBe('{env:ABSENT_VAR}');

delete process.env.PRESENT_VAR;
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Modifying the global process.env object directly in tests can lead to flakiness, especially when tests are run in parallel. It's best practice to isolate tests from each other. Vitest provides vi.stubEnv() for this purpose, which is automatically restored after each test if you use vi.unstubAllEnvs() in an afterEach hook. This makes tests more robust and reliable.

Here's how you could refactor the tests:

import { describe, expect, it, vi, afterEach } from 'vitest';
// ...

describe('applyOverridesToRuntimeConfig environment resolution', () => {
  afterEach(() => {
    vi.unstubAllEnvs();
  });

  it('resolves {env:VAR} placeholders from process.env', () => {
    vi.stubEnv('TEST_VAR', 'secret-value');
    vi.stubEnv('OTHER_VAR', 'other-value');
    // ... rest of test logic, no manual cleanup needed
  });

  it('handles missing environment variables by leaving placeholder intact', () => {
    vi.stubEnv('PRESENT_VAR', 'present');
    // ABSENT_VAR is not stubbed, so it will be undefined
    // ... rest of test logic, no manual cleanup needed
  });
});

MCP secrets are often scrubbed and replaced with {env:VAR} placeholders
by the opencode-synced plugin. However, these placeholders were not
being resolved by opencode at runtime, causing authentication failures.

This change implements a recursive environment variable resolution
mechanism in `applyOverridesToRuntimeConfig` to ensure that these
placeholders are correctly replaced with values from `process.env`.

Added tests to verify resolution of nested placeholders and handling
of missing environment variables.

Co-authored-by: iHildy <25069719+iHildy@users.noreply.github.com>
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.

MCP secret scrubbing — env var placeholders not resolved by opencode

1 participant