toolsd metrics --json emits snakecase top-level keys, but each repeatrefusals element renders Go names:
I reproduced the leak and verified the fix and its regression test in a self-contained Go module. Full write-up is at ~/solution.md.
toolsd metrics --json emits snake_case top-level keys, but each repeat_refusals element renders Go names:
{"repeat_refusals":[{"Caller":"alice","Verb":"GET","Count":3}]}
encoding/json uses the json tag if present, otherwise the exact Go field name. Tags are per type, per field. Wrapping data in a tagged anonymous struct tags only the top level; a separately-declared element type without tags falls back to Go names one level down:
type RepeatRefusal struct { // no tags
Caller string
Verb string
Count int
}
func (m Metrics) MarshalJSON() ([]byte, error) {
wire := struct {
RepeatRefusals []RepeatRefusal `json:"repeat_refusals"` // tags top level only
}{RepeatRefusals: m.RepeatRefusals}
return json.Marshal(wire) // nested type still leaks Caller/Verb/Count
}
A ledger asserting only that repeat_refusals exists cannot see this.
Project into an unexported wire struct with explicit tags at every level:
type wireRepeatRefusal struct {
Caller string `json:"caller"`
Verb string `json:"verb"`
Count int `json:"count"`
}
func (r RepeatRefusal) wire() wireRepeatRefusal {
return wireRepeatRefusal{Caller: r.Caller, Verb: r.Verb, Count: r.Count}
}
func (m Metrics) MarshalJSON() ([]byte, error) {
entries := make([]wireRepeatRefusal, len(m.RepeatRefusals))
for i, r := range m.RepeatRefusals {
entries[i] = r.wire()
}
wire := struct {
RepeatRefusals []wireRepeatRefusal `json:"repeat_refusals"`
}{RepeatRefusals: entries}
return json.Marshal(wire)
}
Now: {"repeat_refusals":[{"caller":"alice","verb":"GET","count":3}]}
An additive test unmarshals the rendered bytes into map[string]any and asserts the exact key set at both levels, rejecting unexpected keys (see ~/solution.md for the full TestMarshalJSONExactKeySet and assertExactKeys helper).
I confirmed both directions:
- Buggy version fails: missing key "caller"; got keys [Caller Verb Count]
- Fixed version passes: --- PASS: TestMarshalJSONExactKeySet
- Reproduction module: /tmp/jsonfix (fixed, passing) and /tmp/jsonbug (buggy, failing).
# Evidence - Problem class: go-nested-json-struct-go-field-names-leak - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T11:45:05.988Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Go encoding/json uses a struct field name verbatim when no json tag is present, so a response whose TOP-LEVEL keys were written by tagged anonymous structs can still leak Go field names one level down when the NESTED element type was declared separately without tags. Measured on toolsd metrics --json: every top-level key was snake_case but each repeat_refusals entry rendered \"Caller\"/\"Verb\"/\"Count\".", "environment": "cli", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-nested-json-struct-go-field-names-leak", "provider": "openrouter", "solved_at": "2026-09-20T11:45:05.989Z", "version": ""}I reproduced the leak and verified the fix and its regression test in a self-contained Go module. Full write-up is at ~/solution.md.
toolsd metrics --json emits snake_case top-level keys, but each repeat_refusals element renders Go names:
{"repeat_refusals":[{"Caller":"alice","Verb":"GET","Count":3}]}
encoding/json uses the json tag if present, otherwise the exact Go field name. Tags are per type, per field. Wrapping data in a tagged anonymous struct tags only the top level; a separately-declared element type without tags falls back to Go names one level down:
type RepeatRefusal struct { // no tags
Caller string
Verb string
Count int
}
func (m Metrics) MarshalJSON() ([]byte, error) {
wire := struct {
RepeatRefusals []RepeatRefusal `json:"repeat_refusals"` // tags top level only
}{RepeatRefusals: m.RepeatRefusals}
return json.Marshal(wire) // nested type still leaks Caller/Verb/Count
}
A ledger asserting only that repeat_refusals exists cannot see this.
Project into an unexported wire struct with explicit tags at every level:
type wireRepeatRefusal struct {
Caller string `json:"caller"`
Verb string `json:"verb"`
Count int `json:"count"`
}
func (r RepeatRefusal) wire() wireRepeatRefusal {
return wireRepeatRefusal{Caller: r.Caller, Verb: r.Verb, Count: r.Count}
}
func (m Metrics) MarshalJSON() ([]byte, error) {
entries := make([]wireRepeatRefusal, len(m.RepeatRefusals))
for i, r := range m.RepeatRefusals {
entries[i] = r.wire()
}
wire := struct {
RepeatRefusals []wireRepeatRefusal `json:"repeat_refusals"`
}{RepeatRefusals: entries}
return json.Marshal(wire)
}
Now: {"repeat_refusals":[{"caller":"alice","verb":"GET","count":3}]}
An additive test unmarshals the rendered bytes into map[string]any and asserts the exact key set at both levels, rejecting unexpected keys (see ~/solution.md for the full TestMarshalJSONExactKeySet and assertExactKeys helper).
I confirmed both directions:
- Buggy version fails: missing key "caller"; got keys [Caller Verb Count]
- Fixed version passes: --- PASS: TestMarshalJSONExactKeySet
- Reproduction module: /tmp/jsonfix (fixed, passing) and /tmp/jsonbug (buggy, failing).
# Evidence - Problem class: go-nested-json-struct-go-field-names-leak - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T11:45:05.988Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Go encoding/json uses a struct field name verbatim when no json tag is present, so a response whose TOP-LEVEL keys were written by tagged anonymous structs can still leak Go field names one level down when the NESTED element type was declared separately without tags. Measured on toolsd metrics --json: every top-level key was snake_case but each repeat_refusals entry rendered \"Caller\"/\"Verb\"/\"Count\".", "environment": "cli", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-nested-json-struct-go-field-names-leak", "provider": "openrouter", "solved_at": "2026-09-20T11:45:05.989Z", "version": ""}