diff --git a/src/cli/config_yaml_edit.c b/src/cli/config_yaml_edit.c index e084b59c1..788752d3a 100644 --- a/src/cli/config_yaml_edit.c +++ b/src/cli/config_yaml_edit.c @@ -2624,8 +2624,18 @@ static int yaml_sequence_line_has_unsupported(const yaml_doc_t *doc, const yaml_ if (value == '#' && (i == start || doc->data[i - YAML_UNIT] == ' ')) { break; } + /* Block-scalar (`|`, `>`) and flow-sequence (`[`, `]`) indicators + * count only where a NODE begins, the same positional rule + * yaml_range_has_unsupported_ex applies to `&`/`*`. Interior ones are + * text: `probe: kaomoji face >w< here` is a plain scalar, and a real + * Hermes config carried two such personas (#1924). `probe: >`, + * `probe: |` and `probe: [a, b]` still begin a value and stay + * unsupported here. */ if (value == '[' || value == ']' || value == '|' || value == '>') { - return YAML_MATCH; + bool begins_node = previous == '\0' || previous == ':' || previous == '-'; + if (begins_node) { + return YAML_MATCH; + } } if (value != ' ' && value != '\t') { previous = value; diff --git a/tests/test_config_yaml_edit.c b/tests/test_config_yaml_edit.c index 3e290240c..27530757a 100644 --- a/tests/test_config_yaml_edit.c +++ b/tests/test_config_yaml_edit.c @@ -1096,6 +1096,109 @@ TEST(config_yaml_edit_bom_before_our_own_section_stays_single_issue1656) { PASS(); } +/* ── #1924: block/flow indicators are indicators only where a value begins ── + * + * The sequence-document scan (the pre_llm_hook_install path) refused `>`, + * `|`, `[` and `]` at ANY position of a plain scalar, so a Hermes persona + * such as `probe: kaomoji face >w< here` made the hook op fail while the + * mcp_servers op on the same file succeeded (bisected to two persona strings + * by @ukind). YAML reads them as indicators only where a value begins — the + * rule the `*`/`&` fix and the quote fix (#1631) already apply. Both real + * Hermes ops run here; our blocks append, so the original document must + * survive as an untouched prefix. */ + +TEST(config_yaml_edit_hermes_accepts_interior_gt_in_plain_scalar_issue1924) { + const char *initial = "agent:\n" + " personalities:\n" + " probe: kaomoji face >w< here\n"; + yaml_fixture_t fixture; + ASSERT_EQ(yaml_fixture_init(&fixture, initial), 0); + ASSERT_EQ(yaml_hermes_upsert(&fixture), CBM_YAML_IDENTITY_EDIT_OK); + ASSERT_EQ(yaml_hermes_hook_upsert(&fixture), CBM_YAML_IDENTITY_EDIT_OK); + char *after = yaml_read_alloc(fixture.path); + ASSERT_NOT_NULL(after); + ASSERT_EQ(strncmp(after, initial, strlen(initial)), 0); + ASSERT_NOT_NULL(strstr(after, "cbm-context")); + free(after); + th_cleanup(fixture.dir); + PASS(); +} + +TEST(config_yaml_edit_hermes_accepts_interior_pipe_in_plain_scalar_issue1924) { + const char *initial = "agent:\n" + " personalities:\n" + " probe: pipe char | mid value\n"; + yaml_fixture_t fixture; + ASSERT_EQ(yaml_fixture_init(&fixture, initial), 0); + ASSERT_EQ(yaml_hermes_upsert(&fixture), CBM_YAML_IDENTITY_EDIT_OK); + ASSERT_EQ(yaml_hermes_hook_upsert(&fixture), CBM_YAML_IDENTITY_EDIT_OK); + char *after = yaml_read_alloc(fixture.path); + ASSERT_NOT_NULL(after); + ASSERT_EQ(strncmp(after, initial, strlen(initial)), 0); + ASSERT_NOT_NULL(strstr(after, "cbm-context")); + free(after); + th_cleanup(fixture.dir); + PASS(); +} + +TEST(config_yaml_edit_hermes_accepts_interior_brackets_in_plain_scalar_issue1924) { + /* In block context `[`/`]` inside a plain scalar are ordinary text; only + * a value-start `[` opens a flow sequence. */ + const char *initial = "agent:\n" + " personalities:\n" + " probe: see docs[1] and args[] here\n"; + yaml_fixture_t fixture; + ASSERT_EQ(yaml_fixture_init(&fixture, initial), 0); + ASSERT_EQ(yaml_hermes_upsert(&fixture), CBM_YAML_IDENTITY_EDIT_OK); + ASSERT_EQ(yaml_hermes_hook_upsert(&fixture), CBM_YAML_IDENTITY_EDIT_OK); + char *after = yaml_read_alloc(fixture.path); + ASSERT_NOT_NULL(after); + ASSERT_EQ(strncmp(after, initial, strlen(initial)), 0); + ASSERT_NOT_NULL(strstr(after, "cbm-context")); + free(after); + th_cleanup(fixture.dir); + PASS(); +} + +TEST(config_yaml_edit_hermes_hook_still_refuses_block_scalar_value_issue1924) { + /* A `>` or `|` that BEGINS a value is a real block scalar whose + * indentation contract this editor does not model: the fail-closed + * refusal stays, and the file stays byte-identical. */ + static const char *const initials[] = { + "agent:\n personalities:\n probe: >\n folded face\n", + "agent:\n personalities:\n probe: |\n literal face\n", + "agent:\n personalities:\n probe: >-\n chomped face\n", + }; + for (size_t i = 0U; i < sizeof(initials) / sizeof(initials[0]); i++) { + yaml_fixture_t fixture; + ASSERT_EQ(yaml_fixture_init(&fixture, initials[i]), 0); + ASSERT_EQ(yaml_hermes_hook_upsert(&fixture), CBM_YAML_IDENTITY_EDIT_ERROR); + char *after = yaml_read_alloc(fixture.path); + ASSERT_NOT_NULL(after); + ASSERT_STR_EQ(after, initials[i]); + free(after); + th_cleanup(fixture.dir); + } + PASS(); +} + +TEST(config_yaml_edit_hermes_hook_still_refuses_nonempty_flow_sequence_value_issue1924) { + /* Pins the #1631 line: exact `[]` is accepted, a non-empty flow sequence + * value is still unsupported in the sequence-document scan. */ + const char *initial = "agent:\n" + " personalities:\n" + " probe: [a, b]\n"; + yaml_fixture_t fixture; + ASSERT_EQ(yaml_fixture_init(&fixture, initial), 0); + ASSERT_EQ(yaml_hermes_hook_upsert(&fixture), CBM_YAML_IDENTITY_EDIT_ERROR); + char *after = yaml_read_alloc(fixture.path); + ASSERT_NOT_NULL(after); + ASSERT_STR_EQ(after, initial); + free(after); + th_cleanup(fixture.dir); + PASS(); +} + /* ── Owned-entry repair (#1631 galaxy + goose upgrades) ────────────────────── * * Byte-identity alone freezes users on any OLD canonical our past writers @@ -1989,6 +2092,11 @@ SUITE(config_yaml_edit) { RUN_TEST(config_yaml_edit_hermes_accepts_interior_apostrophe_and_plain_wrap_issue1631); RUN_TEST(config_yaml_edit_accepts_utf8_bom_issue1656); RUN_TEST(config_yaml_edit_bom_before_our_own_section_stays_single_issue1656); + RUN_TEST(config_yaml_edit_hermes_accepts_interior_gt_in_plain_scalar_issue1924); + RUN_TEST(config_yaml_edit_hermes_accepts_interior_pipe_in_plain_scalar_issue1924); + RUN_TEST(config_yaml_edit_hermes_accepts_interior_brackets_in_plain_scalar_issue1924); + RUN_TEST(config_yaml_edit_hermes_hook_still_refuses_block_scalar_value_issue1924); + RUN_TEST(config_yaml_edit_hermes_hook_still_refuses_nonempty_flow_sequence_value_issue1924); RUN_TEST(config_yaml_edit_repairs_prior_unquoted_command_entry_issue1631); RUN_TEST(config_yaml_edit_repairs_prior_goose_block_without_name); RUN_TEST(config_yaml_edit_still_refuses_truly_foreign_entry_under_our_key);