fix(config): refuse scalar writes to list-valued keys in config set - #76139
fix(config): refuse scalar writes to list-valued keys in config set#76139paoloantinori wants to merge 4 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the scalar-write hazard and adding focused coverage.
Problems
- The new
DEFAULT_CONFIGlookup does not cover the reported path.pluginsis intentionally absent fromDEFAULT_CONFIG(hermes_cli/config.py:1844-1858), soplugins.enabledbypasses the added guard and still reaches the scalar assignment inset_config_value()(hermes_cli/config.py:4948). Plugin loading treats a non-list allow-list as malformed (hermes_cli/plugins.py:243-268). website/docs/user-guide/features/browser.md:422currently documentshermes config set toolsets '["hermes-cli", "browser"]'; becausetoolsetsis a list default (hermes_cli/config_defaults.py:12), this change rejects the documented command.
Suggested changes
- Cover
plugins.enabled/plugins.disabledthrough an appropriate schema source and add a regression test that verifies rejection leaves the file unchanged. - Reconcile the documented
toolsetscommand with the chosen list-setting behavior.
Automated hermes-sweeper review.
| # Guard: refuse scalar writes to list-valued keys. `hermes config set` | ||
| # writes a single scalar, which corrupts list-typed settings (e.g. | ||
| # `plugins.enabled` becomes a quoted string instead of a YAML list). | ||
| _list_default = _default_value_for_key(key) |
There was a problem hiding this comment.
plugins is intentionally absent from DEFAULT_CONFIG (hermes_cli/config.py:1844-1858), so _default_value_for_key("plugins.enabled") returns None. This leaves the issue's reported command unguarded; please cover the plugin allow/deny lists through a schema source that includes them.
|
Thanks @teknium1, both findings addressed in
79 tests pass (74 existing + 5 new: JSON array parse, comma-separated parse, existing-list-key detection, malformed-JSON refusal, scalar key unaffected). Self-reviewed for reuse/simplification/efficiency/altitude; the one real gap (first-set of a DEFAULT_CONFIG-absent key with non- |
SummaryThree PRs address the same scalar-write corruption in Related pull requests
Duplicates#76139 and #76234 target the same #76138 write path; revised #76139 subsumes #76234's list-key detection while choosing parsing instead of refusal. #37460 addresses the same underlying coercion defect more narrowly and contributes distinct YAML-bracket parsing cases that should be salvaged into #76139. Suggested consolidationKeep #76139 open with a salvage path: add a regression for first-time Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I37455(["issue #37455 (open)"])
I76138(["issue #76138 (open)"])
subgraph Dup37460 ["PRs duplicating each other"]
P37460["PR #37460 (open)"]
P76139["PR #76139 (open)"]
end
P76139 -->|best fix| I37455
P76139 -->|best fix| I76138
class I37455 open
class I76138 open
class P37460 open
class P76139 open
class P37460 best
class P76139 best
class P76139 best
class P76139 target
click I37455 "https://github.com/NousResearch/hermes-agent/issues/37455"
click I76138 "https://github.com/NousResearch/hermes-agent/issues/76138"
click P37460 "https://github.com/NousResearch/hermes-agent/pull/37460"
click P76139 "https://github.com/NousResearch/hermes-agent/pull/76139"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 3 pull requests and 2 issues in this complex. Each diff was read against this issue; Assessment working set: 15 kB of PR diffs, 9 kB of issue/PR text, 8 kB of discussion (13 comments), 10 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
…d, unconditional [)
…ow lists
/simplify pass on the rebased branch (3 reviewers, unanimous direction):
- The hand-rolled user_config walk is replaced by _get_nested (which
also resolves list-index paths like custom_providers.0).
- _parse_list_value parses list literals via yaml.safe_load instead of
json.loads: JSON is a YAML subset, and YAML flow lists like
"[a, b]" (unquoted) now parse instead of being refused. One parser
for the [ shape instead of two (the structured-value elif keeps { and
block-style; leading-[ never reaches it anymore, and its comment no
longer claims otherwise).
- Refusal message no longer asserts the key is list-valued when the
trigger was only the leading-[ intent.
- Guard tests share a _set_and_load helper; new test pins the YAML
flow-list acceptance.
- Multi-line YAML block input ("- curl\n- git") on a list-defaulted key
parses via the shared yaml path instead of collapsing into a one-
element list (regression vs main found by running both trees).
- Leading-[ intent no longer fires for known string-typed keys: their
bracket-looking values (shell one-liners starting with [[) stay
strings, restoring the e4ea0a0 guarantee the intent bypass broke.
- Intent-only triggers whose literal fails to parse fall through to the
coercion chain (stored as string with main's warning) instead of hard-
refusing open keys like quick_commands.check; the hard refusal now
applies only where corruption is guaranteed (list-typed keys).
- Non-string values (programmatic callers passing a real list) pass
through untouched instead of crashing on .strip().
- Boundary tests for each case.
0d64dc9 to
63e2d28
Compare
|
Heads-up on the red checks: across two CI runs on this head, three different gateway tests failed (slice 6/8: |
|
Update for whoever reruns/merges: CI run 32116384893 at this head is fully green across all slices; the red slice entries in the rollup come from parallel flake runs (different gateway test each time, none config-related, none reproducible locally). Main's own CI has been red on the same gateway-goal tests today as well. |
Fixes #76138.
Summary
During a coding session, a Hermes agent ran
hermes config seton a list-valued key (plugins.enabled), which wrote a scalar string into config.yaml instead of a YAML list, silently corrupting the config. The function had no awareness of the target key's schema type.Changes
set_config_value(hermes_cli/config.py): before coercing/writing, check_default_value_for_key(key)againstDEFAULT_CONFIG. If the default is alist, refuse with a clear error that shows the correct YAML list form and directs the user tohermes config edit. This mirrors the existing scalar-over-mapping guard (which refuses dict-section overwrites without--force).Test plan
toolsets) refused with "list-valued" in the error; scalar key (terminal.backend) still set normally.Backward compatibility
The guard fires only for keys whose
DEFAULT_CONFIGdefault is a list. Unknown keys and scalar keys are unaffected. Users who need to set list values editconfig.yamldirectly (which is already the documented path for complex values).