mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-26 06:01:49 +00:00
fix(codex-runtime): keep migrated root keys top-level
This commit is contained in:
parent
13c72fb486
commit
274217316e
2 changed files with 54 additions and 9 deletions
|
|
@ -304,6 +304,37 @@ def render_codex_toml_section(
|
||||||
return "\n".join(out) + "\n"
|
return "\n".join(out) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_managed_block_at_top_level(user_text: str, managed_block: str) -> str:
|
||||||
|
"""Insert Hermes' managed Codex TOML block while keeping root keys root-scoped.
|
||||||
|
|
||||||
|
TOML has no syntax to return to the document root after a table header.
|
||||||
|
Therefore appending a root key like `default_permissions = ...` after a
|
||||||
|
user table such as `[features]` actually creates `features.default_permissions`,
|
||||||
|
which Codex rejects. Insert the managed block before the first table header
|
||||||
|
so its root keys remain top-level, while preserving user content verbatim.
|
||||||
|
"""
|
||||||
|
if not user_text.strip():
|
||||||
|
return managed_block
|
||||||
|
|
||||||
|
lines = user_text.splitlines(keepends=True)
|
||||||
|
first_table_idx: Optional[int] = None
|
||||||
|
for idx, line in enumerate(lines):
|
||||||
|
stripped = line.lstrip()
|
||||||
|
if stripped.startswith("["):
|
||||||
|
first_table_idx = idx
|
||||||
|
break
|
||||||
|
|
||||||
|
if first_table_idx is None:
|
||||||
|
prefix = user_text.rstrip("\n")
|
||||||
|
return f"{prefix}\n\n{managed_block}" if prefix else managed_block
|
||||||
|
|
||||||
|
prefix = "".join(lines[:first_table_idx]).rstrip("\n")
|
||||||
|
suffix = "".join(lines[first_table_idx:]).lstrip("\n")
|
||||||
|
if prefix:
|
||||||
|
return f"{prefix}\n\n{managed_block}\n{suffix}"
|
||||||
|
return f"{managed_block}\n{suffix}"
|
||||||
|
|
||||||
|
|
||||||
def _strip_existing_managed_block(toml_text: str) -> str:
|
def _strip_existing_managed_block(toml_text: str) -> str:
|
||||||
"""Remove any prior managed section so re-runs idempotently replace it.
|
"""Remove any prior managed section so re-runs idempotently replace it.
|
||||||
|
|
||||||
|
|
@ -571,14 +602,7 @@ def migrate(
|
||||||
report.errors.append(f"could not read {target}: {exc}")
|
report.errors.append(f"could not read {target}: {exc}")
|
||||||
return report
|
return report
|
||||||
without_managed = _strip_existing_managed_block(existing)
|
without_managed = _strip_existing_managed_block(existing)
|
||||||
# Ensure exactly one blank line between user content and managed block
|
new_text = _insert_managed_block_at_top_level(without_managed, managed_block)
|
||||||
if without_managed and not without_managed.endswith("\n"):
|
|
||||||
without_managed += "\n"
|
|
||||||
new_text = (
|
|
||||||
without_managed.rstrip("\n") + "\n\n" + managed_block
|
|
||||||
if without_managed.strip()
|
|
||||||
else managed_block
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
new_text = managed_block
|
new_text = managed_block
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -567,10 +567,31 @@ class TestMigrate:
|
||||||
assert "[model]" in new_text
|
assert "[model]" in new_text
|
||||||
assert 'profile = "default"' in new_text
|
assert 'profile = "default"' in new_text
|
||||||
assert "[providers.openai]" in new_text
|
assert "[providers.openai]" in new_text
|
||||||
# And new MCP block appended
|
# And new MCP block inserted without breaking user tables
|
||||||
assert "[mcp_servers.a]" in new_text
|
assert "[mcp_servers.a]" in new_text
|
||||||
assert MIGRATION_MARKER in new_text
|
assert MIGRATION_MARKER in new_text
|
||||||
|
|
||||||
|
def test_managed_root_keys_stay_top_level_when_config_ends_in_table(self, tmp_path):
|
||||||
|
"""TOML has no explicit 'leave current table' syntax. If Hermes appends
|
||||||
|
root keys like default_permissions after a user table such as [features],
|
||||||
|
Codex parses them as features.default_permissions and rejects the config.
|
||||||
|
The managed block must therefore be inserted before the first table."""
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
target = tmp_path / "config.toml"
|
||||||
|
target.write_text(
|
||||||
|
'model = "gpt-5.5"\n'
|
||||||
|
"\n"
|
||||||
|
"[features]\n"
|
||||||
|
"terminal_resize_reflow = true\n"
|
||||||
|
)
|
||||||
|
migrate({}, codex_home=tmp_path, discover_plugins=False, expose_hermes_tools=False)
|
||||||
|
new_text = target.read_text()
|
||||||
|
parsed = tomllib.loads(new_text)
|
||||||
|
assert parsed["default_permissions"] == ":workspace"
|
||||||
|
assert "default_permissions" not in parsed["features"]
|
||||||
|
assert new_text.index(MIGRATION_MARKER) < new_text.index("[features]")
|
||||||
|
|
||||||
def test_preserves_user_mcp_server_outside_managed_block(self, tmp_path):
|
def test_preserves_user_mcp_server_outside_managed_block(self, tmp_path):
|
||||||
"""Quirk #6: when a user adds their own MCP server entry directly
|
"""Quirk #6: when a user adds their own MCP server entry directly
|
||||||
to ~/.codex/config.toml outside Hermes' managed block, re-running
|
to ~/.codex/config.toml outside Hermes' managed block, re-running
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue