feat: add YAML support for persona files #58

Merged
aweiker merged 6 commits from issue-57 into main 2026-05-11 01:39:43 +00:00
2 changed files with 43 additions and 11 deletions
Showing only changes of commit 6035afeea7 - Show all commits
+25 -10
View File
6
@@ -34,21 +34,36 @@ func parsePersona(data []byte, source string) (*Persona, error) {
### YAML Parsing with Depth Protection
Review

[NIT] The design doc mentions updating the embed directive to include both formats, but the code embeds only *.yaml (and built-in JSON files were removed). Consider updating the doc to reflect the final decision to embed YAML only.

**[NIT]** The design doc mentions updating the embed directive to include both formats, but the code embeds only *.yaml (and built-in JSON files were removed). Consider updating the doc to reflect the final decision to embed YAML only.
```go
func parseYAML(data []byte, source string) (*Persona, error) {
var p Persona
// gopkg.in/yaml.v3 does NOT have built-in depth limiting.
// Use explicit depth check via yaml.Node API.
if err := yaml.Unmarshal(data, &p); err != nil {
return nil, fmt.Errorf("parse persona %s: %w", source, err)
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {
var node yaml.Node
dec := yaml.NewDecoder(bytes.NewReader(data))
if err := dec.Decode(&node); err != nil {
return err
}
if err := validatePersona(&p, source); err != nil {
return nil, err
if err := checkYAMLDepth(&node, 0, maxDepth); err != nil {
return err
}
return &p, nil
return node.Decode(out)
}
Review

[NIT] The design doc's error cases table says 'Deeply nested YAML | Library rejects (v1.16.0+ fix)' but the actual implementation uses explicit depth checking via checkYAMLDepth, not a library-level fix. The implementation is correct (and better), but the design doc description is inaccurate.

**[NIT]** The design doc's error cases table says 'Deeply nested YAML | Library rejects (v1.16.0+ fix)' but the actual implementation uses explicit depth checking via `checkYAMLDepth`, not a library-level fix. The implementation is correct (and better), but the design doc description is inaccurate.
func checkYAMLDepth(node *yaml.Node, depth, maxDepth int) error {
if depth > maxDepth {
return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth)
}
// Handle alias nodes by following the Alias pointer
if node.Kind == yaml.AliasNode && node.Alias != nil {
return checkYAMLDepth(node.Alias, depth, maxDepth)
Review

[NIT] The design doc's Error Cases table says 'Deeply nested YAML | Library rejects (v1.16.0+ fix)' — but the actual implementation does not rely on library-level rejection; it implements its own depth check. This entry is misleading and should say 'Application-level depth check rejects' to accurately reflect the implementation.

**[NIT]** The design doc's Error Cases table says 'Deeply nested YAML | Library rejects (v1.16.0+ fix)' — but the actual implementation does *not* rely on library-level rejection; it implements its own depth check. This entry is misleading and should say 'Application-level depth check rejects' to accurately reflect the implementation.
Review

[NIT] The design doc's Error Cases table says 'Deeply nested YAML | Library rejects (v1.16.0+ fix)' but the actual implementation handles this via the custom checkYAMLDepth walker, not the library itself. The description is misleading and may cause confusion during future maintenance.

**[NIT]** The design doc's Error Cases table says 'Deeply nested YAML | Library rejects (v1.16.0+ fix)' but the actual implementation handles this via the custom `checkYAMLDepth` walker, not the library itself. The description is misleading and may cause confusion during future maintenance.
}
for _, child := range node.Content {
if err := checkYAMLDepth(child, depth+1, maxDepth); err != nil {
return err
}
}
return nil
}
```
The `gopkg.in/yaml.v3` library does not have built-in depth protection, so we implement explicit depth checking by first decoding into a `yaml.Node`, walking the tree to verify depth, then decoding into the target struct.
The `gopkg.in/yaml.v3` library does not have built-in depth protection, so we implement explicit depth checking by first decoding into a `yaml.Node`, walking the tree to verify depth (including alias resolution), then decoding into the target struct.
## State/Data Model
1
+18 -1
View File
6
@@ -52,6 +52,9 @@ func LoadPersona(path string) (*Persona, error) {
if err != nil {
return nil, fmt.Errorf("read persona file %s: %w", path, err)
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("persona file %s is not a regular file", path)
}
if info.Size() > MaxPersonaFileSize {
return nil, fmt.Errorf("persona file %s exceeds maximum size (%d bytes)", path, MaxPersonaFileSize)
}
@@ -59,6 +62,11 @@ func LoadPersona(path string) (*Persona, error) {
if err != nil {
return nil, fmt.Errorf("read persona file %s: %w", path, err)
}
// Re-check size after read to defend against TOCTOU races where file
// grows between stat and read (e.g., appending process, replaced file).
if len(data) > MaxPersonaFileSize {
return nil, fmt.Errorf("persona file %s exceeds maximum size (%d bytes)", path, MaxPersonaFileSize)
}
return parsePersona(data, path)
}
3
@@ -144,7 +152,10 @@ func parsePersona(data []byte, source string) (*Persona, error) {
// unmarshalYAMLWithDepthLimit unmarshals YAML data with explicit depth limiting.
// This protects against stack exhaustion from deeply nested structures.
func unmarshalYAMLWithDepthLimit(data []byte, out interface{}, maxDepth int) error {
// Note: Multi-document YAML files are accepted but only the first document is
// parsed; additional documents are silently ignored. This is acceptable for
// persona files where multi-document support is not a use case.
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {
var node yaml.Node
dec := yaml.NewDecoder(bytes.NewReader(data))
if err := dec.Decode(&node); err != nil {
@@ -159,10 +170,16 @@ func unmarshalYAMLWithDepthLimit(data []byte, out interface{}, maxDepth int) err
}
// checkYAMLDepth recursively checks that YAML nodes don't exceed the depth limit.
// Handles alias nodes by following the Alias pointer to check the target's depth.
func checkYAMLDepth(node *yaml.Node, depth, maxDepth int) error {
if depth > maxDepth {
return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth)
}
// Handle alias nodes: follow the alias to its anchor target.
// The alias itself doesn't add depth, but we must check the target.
if node.Kind == yaml.AliasNode && node.Alias != nil {
return checkYAMLDepth(node.Alias, depth, maxDepth)
}
for _, child := range node.Content {
if err := checkYAMLDepth(child, depth+1, maxDepth); err != nil {
return err
1