Repository navigation
fix(codex): forward ${VAR} stdio env values with env_vars instead of writing them #3406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Abhishek Acharya (abhishekacharya200)
wants to merge
2
commits into
microsoft:main
Choose a base branch
from
abhishekacharya200:fix/codex-mcp-env-vars
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -219,6 +219,26 @@ def matches_managed_server_config(self, native: object, expected: dict) -> bool: | |
| comparable.pop("id") | ||
| return comparable == expected | ||
|
|
||
| def matches_legacy_managed_server_config(self, native: object, expected: dict) -> bool: | ||
| """Recognize a stdio entry written before ``${VAR}`` values moved to ``env_vars``. | ||
|
|
||
| The older formatter stored either the resolved value or the literal | ||
| reference, so anything else is a manual edit and stays a conflict. | ||
| """ | ||
| names = expected.get("env_vars") | ||
| native_env = native.get("env") if isinstance(native, dict) else None | ||
| if not names or not isinstance(native_env, dict) or "env_vars" in native: | ||
| return False | ||
| legacy_env = dict(expected.get("env") or {}) | ||
| for name in names: | ||
| value = native_env.get(name) | ||
| if value is None or value not in (f"${{{name}}}", os.getenv(name)): | ||
| return False | ||
| legacy_env[name] = value | ||
| legacy = {key: value for key, value in expected.items() if key != "env_vars"} | ||
| legacy["env"] = legacy_env | ||
| return self.matches_managed_server_config(native, legacy) | ||
|
|
||
| def update_config(self, config_updates): | ||
| """Update the Codex CLI MCP configuration. | ||
|
|
||
|
|
@@ -363,17 +383,29 @@ def _format_server_config(self, server_info, env_overrides=None, runtime_vars=No | |
| # syntaxes (``<VAR>``, ``${VAR}``, ``${env:VAR}``) are resolved at | ||
| # install time before being written to ~/.codex/config.toml. | ||
| # See issue #1266. | ||
| # | ||
| # A value authored as exactly ``${VAR}`` under the key ``VAR`` is the | ||
| # one case that must NOT be resolved and stored: Codex reads those from | ||
| # the launching environment through ``env_vars`` at server start, so the | ||
| # secret never lands in config.toml. See issue #3393. | ||
| raw = server_info.get("_raw_stdio") | ||
| if raw: | ||
| config["command"] = raw["command"] | ||
| if raw.get("cwd") is not None: | ||
| config["cwd"] = raw["cwd"] | ||
| forwarded_env: list[str] = [] | ||
| resolved_env_for_args: dict = {} | ||
| written_env: dict = {} | ||
| if raw.get("env"): | ||
| resolved_env_for_args = self._resolve_environment_variables( | ||
| raw["env"], env_overrides=env_overrides | ||
| literal_env, forwarded_env = self._split_forwarded_stdio_env(raw["env"]) | ||
| written_env = self._resolve_environment_variables( | ||
| literal_env, env_overrides=env_overrides | ||
| ) | ||
| config["env"] = resolved_env_for_args | ||
| # Args keep resolving against every declared name; a forwarded | ||
| # value is read from the environment when it is available, and | ||
| # never prompts for a value that only the runtime needs. | ||
| resolved_env_for_args = dict(written_env) | ||
| for name in forwarded_env: | ||
| value = (env_overrides or {}).get(name) or os.getenv(name) | ||
| if value: | ||
| resolved_env_for_args[name] = value | ||
| self._warn_input_variables(raw["env"], server_info.get("name", ""), "Codex CLI") | ||
|
|
||
| def _process_stdio_arg(arg): | ||
|
|
@@ -383,9 +415,17 @@ def _process_stdio_arg(arg): | |
| ) | ||
| return self.normalize_project_arg(arg) | ||
|
|
||
| config["args"] = [_process_stdio_arg(arg) for arg in raw.get("args") or []] | ||
| self._merge_extra(config, server_info) | ||
| return config | ||
| # Smoke scalars and arrays before the [env] sub-table: tomlkit nests | ||
| # any later key inside the table it has already opened. | ||
| stdio_config: dict = {"command": raw["command"]} | ||
| if raw.get("cwd") is not None: | ||
| stdio_config["cwd"] = raw["cwd"] | ||
| stdio_config["args"] = [_process_stdio_arg(arg) for arg in raw.get("args") or []] | ||
| if forwarded_env: | ||
| stdio_config["env_vars"] = forwarded_env | ||
| stdio_config["env"] = written_env | ||
| self._merge_extra(stdio_config, server_info) | ||
|
Comment on lines
+424
to
+427
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 95c6d93. |
||
| return stdio_config | ||
|
|
||
| # Remote MCP handling. | ||
| # Precedence on Codex when a server publishes BOTH a remote and a stdio | ||
|
|
@@ -656,6 +696,31 @@ def _process_arguments( # pylint: disable=duplicate-code # structural similari | |
|
|
||
| return processed | ||
|
|
||
| @staticmethod | ||
| def _split_forwarded_stdio_env(raw_env: dict) -> tuple[dict, list[str]]: | ||
| """Split a self-defined stdio env block into literals and forwarded names. | ||
|
|
||
| Codex reads a stdio server's variables from the environment that launches | ||
| it when they are named in ``env_vars``, so a value written as exactly | ||
| ``${VAR}`` under the key ``VAR`` does not have to be resolved and stored | ||
| in config.toml. Everything else keeps the existing install-time | ||
| resolution and is written to ``[mcp_servers.<name>.env]``. | ||
|
|
||
| Returns: | ||
| tuple: ``(literal_env, forwarded_env)`` -- the entries to resolve and | ||
| the variable names to forward. | ||
| """ | ||
| literal_env: dict = {} | ||
| forwarded_env: list[str] = [] | ||
| for name, raw_value in raw_env.items(): | ||
| if isinstance(raw_value, str): | ||
| reference = _ENV_VAR_RE.fullmatch(raw_value) | ||
| if reference and reference.group(1) == name: | ||
| forwarded_env.append(name) | ||
| continue | ||
| literal_env[name] = raw_value | ||
| return literal_env, forwarded_env | ||
|
|
||
| def _process_environment_variables(self, env_vars, env_overrides=None): | ||
| """Process environment variable definitions and resolve actual values. | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 95c6d93. A new adapter hook,
matches_legacy_managed_server_config, lets the self defined branch ofcompare_native_mcp_connectiontreat an entry from the older formatter as an update. It only accepts a value that is the literal${VAR}or the current environment value, since that is all the old formatter could write. Anything else stays a conflict, so manual edits are kept. One limit: if the variable changed after the old write, the entry also reports a conflict, which is the safe side. Tests are intest_mcp_ownership.py.