fix(mcp): fold YAML block-scalar continuations before validating the envelope
Round-6 review found a bypass in a different mechanism than rounds 1-6's fence-detection family: frontmatterScalars's indentation guard skips any indented continuation line outright, so old_str="half_life: durable", new_str="half_life:\n garbage-value" turns the field into a YAML block-scalar shape whose real value lives on the continuation line. present()'s own "empty value = placeholder, not an assertion" contract then treats the field as absent, and claim.ValidateEnvelope never sees the invalid value at all — no encoding quirk needed, one ordinary brain_edit call smuggles an out-of-enum value straight past the check. Fold indented continuation lines into the value when a top-level key's inline value is empty, so oneOf() still sees what was actually written. A genuinely empty value (no indented line follows) still folds to "", preserving the existing placeholder-not-assertion behavior for legitimately unset fields. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -344,7 +344,9 @@ func frontmatterScalars(doc string) map[string]string {
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
for _, line := range strings.Split(block, "\n") {
|
||||
lines := strings.Split(block, "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
line := lines[i]
|
||||
if line != "" && (line[0] == ' ' || line[0] == '\t') {
|
||||
continue
|
||||
}
|
||||
@@ -352,7 +354,31 @@ func frontmatterScalars(doc string) map[string]string {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[strings.TrimSpace(key)] = strings.TrimSpace(val)
|
||||
val = strings.TrimSpace(val)
|
||||
if val == "" {
|
||||
// Round 6: a colon with nothing after it is ambiguous — it can
|
||||
// be a genuinely empty/omitted value, or the start of a YAML
|
||||
// block scalar whose real value lives on the indented
|
||||
// continuation line(s) that follow (e.g. "half_life:\n
|
||||
// durable"). Treating the latter as absent let an edit smuggle
|
||||
// an invalid value straight past ValidateEnvelope: the field
|
||||
// read as unset while the disk actually carried a value. Fold
|
||||
// the continuation in as a best-effort join so oneOf() still
|
||||
// sees it. A genuinely empty value (no indented line follows)
|
||||
// still folds to "", preserving present()'s existing
|
||||
// placeholder-not-assertion contract.
|
||||
var cont []string
|
||||
for i+1 < len(lines) {
|
||||
next := lines[i+1]
|
||||
if next == "" || (next[0] != ' ' && next[0] != '\t') {
|
||||
break
|
||||
}
|
||||
cont = append(cont, strings.TrimSpace(next))
|
||||
i++
|
||||
}
|
||||
val = strings.Join(cont, " ")
|
||||
}
|
||||
out[strings.TrimSpace(key)] = val
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user