address self-review findings on PR #89
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 39s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m12s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m33s
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 17s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 39s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m12s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m33s
MINOR fixes:
- docs/DESIGN-57-yaml-persona.md: fix Error Cases table entry to reflect
custom AST walk (checkYAMLDepth) instead of stale library-level reference
- review/persona.go: add EOF check after JSON decode to reject trailing
garbage after a valid JSON object (prevents silent acceptance of malformed
input like '{"name":"x"}garbage')
- review/persona_test.go: add TestJSONTrailingContentRejected test
NIT fixes:
- review/persona.go: add default case to checkYAMLDepth switch with
explanatory comment about scalar leaf nodes
- review/persona.go: document AnchorNode depth+1 conservative asymmetry
- review/persona.go: simplify redundant if-guard in ListBuiltinPersonas
This commit is contained in:
@@ -53,7 +53,7 @@ No new state. Same `Persona` struct, just different parsing.
|
||||
| Error | Handling |
|
||||
|-------|----------|
|
||||
| Invalid YAML syntax | Return parse error with source file |
|
||||
| Deeply nested YAML | Library rejects (v1.16.0+ fix) |
|
||||
| Deeply nested YAML | Custom AST walk (`checkYAMLDepth`) rejects before decode |
|
||||
| Unknown extension | Fall back to JSON parsing |
|
||||
| Missing required fields | Validation rejects after parse |
|
||||
|
||||
|
||||
+21
-5
@@ -5,6 +5,7 @@ import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -120,9 +121,7 @@ func ListBuiltinPersonas() []string {
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if !seen[personaName] {
|
||||
seen[personaName] = true
|
||||
}
|
||||
seen[personaName] = true
|
||||
}
|
||||
names := make([]string, 0, len(seen))
|
||||
for name := range seen {
|
||||
@@ -148,6 +147,15 @@ func parsePersona(data []byte, source string) (*Persona, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
err = dec.Decode(&p)
|
||||
if err == nil {
|
||||
// Reject trailing content after the first valid JSON object.
|
||||
// Without this check, input like `{"name":"x"}garbage` would
|
||||
// silently succeed because Decoder stops after one object.
|
||||
var dummy json.RawMessage
|
||||
if dec.More() || dec.Decode(&dummy) != io.EOF {
|
||||
err = fmt.Errorf("unexpected trailing content after JSON object")
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse persona %s: %w", source, err)
|
||||
@@ -275,6 +283,12 @@ func checkYAMLDepth(node ast.Node, depth, maxDepth, maxNodes int, validated map[
|
||||
return err
|
||||
}
|
||||
case *ast.AnchorNode:
|
||||
// Increment depth for anchor values as a conservative measure: the
|
||||
// anchor definition itself is structural, and treating it as a depth
|
||||
// level ensures that deeply nested anchors are caught at definition
|
||||
// time rather than only when referenced via alias. This +1 is
|
||||
// asymmetric with alias (which also increments) — by design, the
|
||||
// combined budget is halved for anchored content that is later aliased.
|
||||
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -282,8 +296,10 @@ func checkYAMLDepth(node ast.Node, depth, maxDepth, maxNodes int, validated map[
|
||||
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
|
||||
return err
|
||||
}
|
||||
// Scalar types (StringNode, IntegerNode, FloatNode, BoolNode, NullNode,
|
||||
// InfinityNode, NanNode, LiteralNode, MergeKeyNode) are leaf nodes.
|
||||
default:
|
||||
// Scalar leaf nodes (StringNode, IntegerNode, FloatNode, BoolNode,
|
||||
// NullNode, InfinityNode, NanNode, LiteralNode, MergeKeyNode) have
|
||||
// no children to recurse into.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -858,3 +858,41 @@ identity: test identity
|
||||
t.Errorf("Name = %q, want %q", p.Name, "test")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONTrailingContentRejected(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
}{
|
||||
{
|
||||
name: "trailing garbage after object",
|
||||
content: `{"name":"test","identity":"test identity"}garbage`,
|
||||
},
|
||||
{
|
||||
name: "two JSON objects",
|
||||
content: `{"name":"test","identity":"test identity"}{"name":"other"}`,
|
||||
},
|
||||
{
|
||||
name: "trailing array",
|
||||
content: `{"name":"test","identity":"test identity"}[]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "test.json")
|
||||
if err := os.WriteFile(path, []byte(tt.content), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
_, err := LoadPersona(path)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for trailing content, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "trailing content") {
|
||||
t.Errorf("error = %q, want to contain 'trailing content'", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user