feat: add YAML support for persona files #58
Reference in New Issue
Block a user
Delete Branch "issue-57"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds YAML support for persona configuration files, providing cleaner multi-line strings and comment support.
Changes
github.com/goccy/go-yamldependency (v1.19.2)parsePersonato detect format by file extension.yamland.ymlextensions (case-insensitive)Why YAML?
YAML provides:
|)JSON remains supported for backwards compatibility.
Closes #57
Self-review: NEEDS_WORK
LoadBuiltinPersonais dead code — JSON files aren't embedded (//go:embed personas/*.yaml) and all builtin personas are now YAML. The comment "Fall back to JSON for backwards compatibility" is misleading. Consider either: (a) removing the dead fallback code for builtins, or (b) updating the embed directive to include both*.yamland*.json. Verdict: Keep as-is for now — the dead code doesn't cause harm and provides future flexibility if someone adds a JSON builtin. Mark as acknowledged.ListBuiltinPersonashandles.ymlfiles butLoadBuiltinPersonaonly tries.yamlextension. This is fine per design ("use.yamlfor consistency") but is a subtle inconsistency. Low priority.Summary: One blocking item — the missing test for deeply nested YAML rejection. The design doc and issue both call for this test as defense-in-depth validation.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
006b7a3b)Sonnet Review
Summary
The PR adds YAML support for persona files, which is a reasonable feature, but it introduces an external dependency (
github.com/goccy/go-yaml) in direct violation of the repository's stated convention of 'Go standard library only — no external dependencies.' This is a hard constraint that must be addressed regardless of how well the rest of the implementation is done.Findings
go.modgithub.com/goccy/go-yaml v1.19.2as a dependency. The repository's CONVENTIONS.md explicitly states 'Go standard library only — no external dependencies.' This PR directly violates that constraint. A stdlib-only YAML parser would need to be implemented manually, or the feature scope should be reconsidered (e.g., keeping only JSON for external files and using a minimal hand-rolled YAML subset, or representing persona files differently). The dependency cannot be added regardless of how useful YAML support would be.review/persona.goListBuiltinPersonasreturns a non-deterministic (map-iteration) order of persona names. While the return value isn't documented as sorted, callers such as the error message inLoadBuiltinPersonajoin the names to display to users, and the non-deterministic order makes tests and error messages flaky. The slice should be sorted withslices.Sort(orsort.Strings) before returning.review/persona.goseenmap inListBuiltinPersonasis used only to deduplicate persona names (it only ever storestrue), but amap[string]struct{}would be more idiomatic. More importantly, the deduplication logic has a subtle inefficiency: it checksif !seen[personaName]before setting totrue, but since the zero value of bool isfalse, the check is redundant —seen[personaName] = trueis sufficient by itself regardless of the current value. The check adds no correctness but implies to readers it does something meaningful.docs/DESIGN-57-yaml-persona.md.yamlextension (not.yml)' as a constraint, but the actual implementation supports both.yamland.yml(and the open questions section documents this decision). The constraint section of the design doc is now inconsistent with the implemented behavior and should be updated to reflect the actual decision.review/persona.goLoadBuiltinPersonahas a comment 'Fall back to JSON for backwards compatibility', but theembeddedPersonasembed directive now only includes*.yamlfiles (//go:embed personas/*.yaml). The JSON files have been deleted. This means the JSON fallback path inLoadBuiltinPersonais dead code — it can never succeed because no.jsonfiles are embedded. The fallback should either be removed or the embed directive should include*.jsonif JSON built-in personas need to be supported.review/persona_test.goTestLoadPersonaCaseInsensitiveExtensiontests.YAMLand.YML(uppercase) extensions by creating files with those names and parsing them. On case-insensitive filesystems (macOS) this will work, but on case-sensitive filesystems (Linux CI), creating a file namedtest.YAMLand reading it back is fine — however, this is testing OS-level file creation, not just the parsing logic. The test is correct but would be cleaner usingparsePersonadirectly with a fake source path like"test.YAML"rather than creating real files.Recommendation
REQUEST_CHANGES — REQUEST_CHANGES. The blocking issue is the introduction of
github.com/goccy/go-yamlas an external dependency. The project's conventions explicitly prohibit external dependencies, and this constraint must be respected before the PR can be merged. Options to consider: (1) Implement a minimal YAML parser using only the standard library — this is feasible for the simple struct shape used byPersona, or (2) Use a different approach that avoids YAML entirely, such as supporting multi-line strings in JSON via the existingencoding/jsonpackage (users can use\nescapes, which is admittedly less readable), or (3) Revisit the no-external-dependencies convention through the project's governance process before merging this PR. The dead-code JSON fallback inLoadBuiltinPersona(since*.jsonfiles are no longer embedded) and the non-deterministic sort order inListBuiltinPersonasshould also be fixed. The overall design and test coverage are solid — the implementation is well-structured and the test suite is comprehensive.Review by sonnet
Evaluated against
006b7a3b@@ -0,0 +7,4 @@## Constraints- Backwards compatibility: existing JSON personas must continue to work- Security: protect against DoS via deeply nested YAML (AIKIDO-2024-10486)[MINOR] The design doc says 'Consistency: use
.yamlextension (not.yml)' as a constraint, but the actual implementation supports both.yamland.yml(and the open questions section documents this decision). The constraint section of the design doc is now inconsistent with the implemented behavior and should be updated to reflect the actual decision.@@ -2,2 +2,4 @@go 1.26.2require github.com/goccy/go-yaml v1.19.2 // indirect[MAJOR] Adds
github.com/goccy/go-yaml v1.19.2as a dependency. The repository's CONVENTIONS.md explicitly states 'Go standard library only — no external dependencies.' This PR directly violates that constraint. A stdlib-only YAML parser would need to be implemented manually, or the feature scope should be reconsidered (e.g., keeping only JSON for external files and using a minimal hand-rolled YAML subset, or representing persona files differently). The dependency cannot be added regardless of how useful YAML support would be.@@ -49,0 +52,4 @@yamlFile := name + ".yaml"data, err := embeddedPersonas.ReadFile("personas/" + yamlFile)if err == nil {return parsePersona(data, "builtin:"+yamlFile)[NIT] The fallback to JSON in
LoadBuiltinPersonahas a comment 'Fall back to JSON for backwards compatibility', but theembeddedPersonasembed directive now only includes*.yamlfiles (//go:embed personas/*.yaml). The JSON files have been deleted. This means the JSON fallback path inLoadBuiltinPersonais dead code — it can never succeed because no.jsonfiles are embedded. The fallback should either be removed or the embed directive should include*.jsonif JSON built-in personas need to be supported.@@ -61,3 +73,3 @@return []string{}}var names []stringseen := make(map[string]bool)[MINOR] The
seenmap inListBuiltinPersonasis used only to deduplicate persona names (it only ever storestrue), but amap[string]struct{}would be more idiomatic. More importantly, the deduplication logic has a subtle inefficiency: it checksif !seen[personaName]before setting totrue, but since the zero value of bool isfalse, the check is redundant —seen[personaName] = trueis sufficient by itself regardless of the current value. The check adds no correctness but implies to readers it does something meaningful.@@ -71,1 +89,4 @@personaName = strings.TrimSuffix(name, ".json")default:continue}[MINOR]
ListBuiltinPersonasreturns a non-deterministic (map-iteration) order of persona names. While the return value isn't documented as sorted, callers such as the error message inLoadBuiltinPersonajoin the names to display to users, and the non-deterministic order makes tests and error messages flaky. The slice should be sorted withslices.Sort(orsort.Strings) before returning.@@ -203,6 +309,38 @@ func TestLoadPersonaInvalidJSON(t *testing.T) {}[NIT]
TestLoadPersonaCaseInsensitiveExtensiontests.YAMLand.YML(uppercase) extensions by creating files with those names and parsing them. On case-insensitive filesystems (macOS) this will work, but on case-sensitive filesystems (Linux CI), creating a file namedtest.YAMLand reading it back is fine — however, this is testing OS-level file creation, not just the parsing logic. The test is correct but would be cleaner usingparsePersonadirectly with a fake source path like"test.YAML"rather than creating real files.Security Review
Summary
Overall, the YAML persona support is implemented cleanly with format detection by extension and validation of required fields. The added YAML dependency is pinned and used to unmarshal into a fixed struct, which limits deserialization risk. I see a couple of defense-in-depth improvements around resource exhaustion and supply-chain considerations.
Findings
go.modreview/persona.goreview/persona.goRecommendation
APPROVE — The changes look correct and safe overall, and CI has passed. For defense-in-depth, I recommend adding guardrails around persona file ingestion: (1) enforce a sane maximum persona file size before reading/parsing, and (2) if the YAML library supports it, configure explicit decoder limits (nesting depth, map/array sizes) rather than relying solely on defaults. Also, since this introduces a new third-party dependency, ensure it is included in your regular dependency vulnerability scanning and audit process. With these minor hardening steps, the YAML support should be robust from a security perspective.
Review by security
Evaluated against
006b7a3b@@ -2,2 +2,4 @@go 1.26.2require github.com/goccy/go-yaml v1.19.2 // indirect[MINOR] New third-party dependency (github.com/goccy/go-yaml v1.19.2) increases supply-chain/attack surface. While the version is pinned and includes prior DoS fixes, adding an external parser warrants routine dependency audit and vulnerability monitoring.
@@ -36,2 +37,4 @@// LoadPersona loads a persona from a JSON or YAML file path.// Format is detected by file extension: .yaml/.yml for YAML, .json or other for JSON.func LoadPersona(path string) (*Persona, error) {data, err := os.ReadFile(path)[MINOR] LoadPersona reads the entire persona file with os.ReadFile without size checks. A maliciously large YAML/JSON persona file could cause memory/CPU pressure (DoS) in CI or when run manually. Consider enforcing a reasonable max file size before parsing.
@@ -79,0 +111,4 @@var err errorif isYAML {// go-yaml v1.16.0+ has built-in protection against deeply nested structureserr = yaml.Unmarshal(data, &p)[MINOR] YAML parsing uses yaml.Unmarshal without explicit decoder limits. Although goccy/go-yaml >= v1.16.0 adds default depth protections, setting explicit limits (e.g., maximum nesting depth/collection sizes if supported) would add defense-in-depth against resource exhaustion in pathological inputs.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
006b7a3b)Gpt Review
Summary
YAML support is implemented cleanly with good tests and backward-compatible JSON handling, but the PR introduces a third-party dependency in violation of this repository’s "Go standard library only" policy. Additionally, the new dependency is marked as indirect in go.mod despite being directly imported.
Findings
review/persona.gogo.modgo.modgo mod tidyto classify it as a direct dependency or fix the annotation.review/persona.go*.yaml. Since the JSON files were removed and not embedded, this fallback will never succeed for built-ins and can be simplified or the embed pattern expanded if JSON fallback is intended.review/persona.goRecommendation
REQUEST_CHANGES — The feature is well-implemented with thorough tests and a sensible extension-based format switch, but introducing github.com/goccy/go-yaml violates the repository’s "Go standard library only — no external dependencies" policy and contradicts the README’s "Zero dependencies" claim. Please address this by either: (1) removing the external dependency and reconsidering YAML support (e.g., stick to JSON), (2) gating YAML support behind a build tag or separate tool so the core remains dependency-free, or (3) formally updating repository conventions and README to allow dependencies and documenting the change. Also run
go mod tidyto mark the YAML module as a direct dependency if it remains. If you keep the JSON fallback for built-ins, either embed*.jsonas well or remove the dead fallback path. Optionally, add explicit YAML decoder limits (if available) for defense in depth to match the design notes. Once the dependency policy issue is resolved, this PR should be good to merge.Review by gpt
Evaluated against
006b7a3b@@ -1,3 +1,5 @@module gitea.weiker.me/rodin/review-botgo 1.26.2[MAJOR] Adds third-party dependency github.com/goccy/go-yaml. This conflicts with the documented project rule of no external dependencies.
[MINOR] Dependency is marked as "// indirect" even though yaml is imported in non-test code (review/persona.go). Run
go mod tidyto classify it as a direct dependency or fix the annotation.@@ -8,2 +8,4 @@"strings""unicode/utf8""github.com/goccy/go-yaml"[MAJOR] Introduces external dependency github.com/goccy/go-yaml, which violates the repository convention of using only the Go standard library ("Zero dependencies"). This is a policy-breaking change.
@@ -49,0 +54,4 @@if err == nil {return parsePersona(data, "builtin:"+yamlFile)}[MINOR] Built-in persona loader falls back to JSON if YAML not found, but the embedded FS only includes
*.yaml. Since the JSON files were removed and not embedded, this fallback will never succeed for built-ins and can be simplified or the embed pattern expanded if JSON fallback is intended.@@ -77,2 +110,3 @@var p Personaif err := json.Unmarshal(data, &p); err != nil {var err errorif isYAML {[MINOR] Relying solely on library defaults for YAML depth/alias protections. Consider using an explicit decoder with defensive options (if supported by the library) to enforce a depth/alias limit for defense in depth, aligning with the design doc intent.
Addressing Review Feedback
After careful consideration, I've decided to revert the YAML support entirely rather than try to fix the individual issues while keeping the external dependency.
Reasoning
The MAJOR finding from all reviewers is correct: the
github.com/goccy/go-yamldependency violatesCONVENTIONS.mdwhich states:This is a hard rule. While YAML provides better ergonomics for multi-line strings and comments, the benefit doesn't justify:
The persona files work fine with JSON. Multi-line strings use
\nescapes which is less pretty but acceptable for internal files.Finding Responses
sonnet-review-bot:
security-review-bot:
gpt-review-bot:
self-review (rodin):
Summary
Instead of trying to "fix" the YAML implementation, I'm rejecting the feature entirely. If YAML support is truly needed in the future, the right approach is:
Not: add the dependency first and ask forgiveness later.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
9e15b73a)Sonnet Review
Summary
This PR adds YAML support by introducing an external dependency (
github.com/goccy/go-yaml), which violates the repository's explicit convention of 'Go standard library only — no external dependencies.' The code changes themselves appear to be documentation-only (README.md), but the PR description states a new dependency is being added.Findings
go.mod / go.sum (implied by PR description)Recommendation
REQUEST_CHANGES — The README documentation changes themselves are clean and accurate — they correctly describe the new YAML/JSON dual-format support and the example is well-formatted. However, the underlying implementation relies on an external dependency (
github.com/goccy/go-yaml) which is prohibited by the project's stated conventions. This PR cannot be merged as-is. Options to resolve this: (1) Remove YAML support and keep JSON-only (no convention violation), (2) Implement a YAML parser using only the standard library (difficult and likely impractical for full YAML), (3) Update the repository conventions to allow this specific external dependency if the team decides YAML support is worth the dependency cost. The decision to add an external dependency should be a conscious, documented exception rather than a silent convention break.Review by sonnet
Evaluated against
9e15b73aSecurity Review
Summary
The changes are documentation-only updates to reflect YAML support for persona files. There are no security-relevant code changes in this diff.
Recommendation
APPROVE — Approve as-is. The README updates accurately describe YAML/JSON persona support and present no security impact. CI is passing and no executable code changes are included in this PR segment.
Review by security
Evaluated against
9e15b73aOriginal reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
9e15b73a)Gpt Review
Summary
The README updates clearly introduce YAML persona support, update examples, and adjust descriptions consistently. CI passed and the documentation changes are coherent and well-formatted.
Recommendation
APPROVE — The documentation changes look good and align the README with YAML persona support, including examples and the inputs table. As a follow-up, ensure broader README statements (e.g., any claims about zero external dependencies) remain accurate if YAML parsing required adding an external library, and verify that the described YAML/JSON support matches the implemented behavior across the codebase. Otherwise, this PR is ready to merge.
Review by gpt
Evaluated against
9e15b73a9e15b73a23to7898dd939fSelf-review: NEEDS_WORK
Critical Finding: Documentation-Code Mismatch
[coherence] MAJOR: The PR has a fundamental coherence problem after the revert commit:
9e15b73a) removed ALL YAML support from the codepersona.gouses onlyencoding/json, embedspersonas/*.json, andparsePersona()callsjson.Unmarshalonly.yamlexamples[completeness] MAJOR: The README changes must be reverted since YAML support was removed. The current state documents a feature that doesn't exist.
[solution] MAJOR: Given the feature was rejected (violates "Go standard library only" convention) and the code was reverted, this PR should either:
Verdict
NEEDS_WORK — The PR currently adds false documentation claiming YAML support that was removed. This cannot be merged in its current state.
Recommendation
Close this PR without merging. The feature was correctly rejected per CONVENTIONS.md, but the documentation revert was missed. Opening a separate PR that just reverts the documentation changes would also work.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
7898dd93)Sonnet Review
Summary
The PR adds YAML support for persona files using
gopkg.in/yaml.v3, which is on the project's approved dependency allowlist. However, the design document specifiesgithub.com/goccy/go-yamlas the intended library (citing its v1.16.0+ depth-limit security fix), while the actual implementation uses a different library (gopkg.in/yaml.v3). Additionally,ListBuiltinPersonasreturns a non-deterministic (unordered) slice, and there is a missing test for deeply nested YAML rejection that was explicitly required in the design checklist.Findings
review/persona.godocs/DESIGN-57-yaml-persona.md) explicitly specifiesgithub.com/goccy/go-yamlv1.16.0+ as the required library, citing its built-in protection against deeply nested structures (AIKIDO-2024-10486 / DoS vulnerability fix). The implementation instead usesgopkg.in/yaml.v3, which does NOT have the same built-in depth limiting. The comment inparsePersonasays "go-yaml v1.16.0+ has built-in protection against deeply nested structures" — this is factually incorrect forgopkg.in/yaml.v3. This is a security gap: a maliciously crafted deeply-nested YAML file could cause a stack overflow. Either switch togithub.com/goccy/go-yaml(and add it to the allowlist), or add explicit depth limiting when usinggopkg.in/yaml.v3.review/persona_test.goreview/persona.goListBuiltinPersonasiterates over amap[string]boolto build the names slice, producing non-deterministic ordering. This can cause flaky tests or confusing error messages ("available: docs, security, architect" vs "available: architect, docs, security" depending on map iteration order). The slice should be sorted before returning:sort.Strings(names).docs/DESIGN-57-yaml-persona.mdgithub.com/goccy/go-yamlbut the implementation usesgopkg.in/yaml.v3. The design document is now inaccurate/misleading. If the decision was made to usegopkg.in/yaml.v3instead, the design document should be updated to reflect this, and the security claim about depth protection should be addressed explicitly.review/persona.go// go-yaml v1.16.0+ has built-in protection against deeply nested structuresis misleading/incorrect forgopkg.in/yaml.v3. It appears to have been written forgoccy/go-yamland copied over without updating. This should either be removed or corrected to accurately describegopkg.in/yaml.v3's actual behavior.Recommendation
REQUEST_CHANGES — There is a significant mismatch between the stated security design (using
goccy/go-yamlwith depth protection) and the actual implementation (usinggopkg.in/yaml.v3without the claimed depth protection). This matters because the PR explicitly cites protection against a YAML DoS vulnerability as a constraint. Before approving:github.com/goccy/go-yamlas specified in the design — this requires adding it to the dependency allowlist per the convention (PR to CONVENTIONS.md first, then use it here).gopkg.in/yaml.v3but remove the false security claim and add explicit depth-limit handling (e.g., parse into a genericinterface{}first, check nesting depth, then unmarshal into the struct — or accept the known limitation and document it).ListBuiltinPersonasreturn value for determinism.Review by sonnet
Evaluated against
7898dd93@@ -0,0 +11,4 @@- Consistency: use `.yaml` extension (not `.yml`)- Library: use `github.com/goccy/go-yaml` v1.16.0+ (actively maintained, security fix applied)## Proposed Approach[MINOR] The design document says to use
github.com/goccy/go-yamlbut the implementation usesgopkg.in/yaml.v3. The design document is now inaccurate/misleading. If the decision was made to usegopkg.in/yaml.v3instead, the design document should be updated to reflect this, and the security claim about depth protection should be addressed explicitly.@@ -9,2 +9,4 @@"unicode/utf8""gopkg.in/yaml.v3")[MAJOR] The design document (
docs/DESIGN-57-yaml-persona.md) explicitly specifiesgithub.com/goccy/go-yamlv1.16.0+ as the required library, citing its built-in protection against deeply nested structures (AIKIDO-2024-10486 / DoS vulnerability fix). The implementation instead usesgopkg.in/yaml.v3, which does NOT have the same built-in depth limiting. The comment inparsePersonasays "go-yaml v1.16.0+ has built-in protection against deeply nested structures" — this is factually incorrect forgopkg.in/yaml.v3. This is a security gap: a maliciously crafted deeply-nested YAML file could cause a stack overflow. Either switch togithub.com/goccy/go-yaml(and add it to the allowlist), or add explicit depth limiting when usinggopkg.in/yaml.v3.@@ -72,0 +94,4 @@seen[personaName] = true}}var names []string[MINOR]
ListBuiltinPersonasiterates over amap[string]boolto build the names slice, producing non-deterministic ordering. This can cause flaky tests or confusing error messages ("available: docs, security, architect" vs "available: architect, docs, security" depending on map iteration order). The slice should be sorted before returning:sort.Strings(names).@@ -79,1 +116,4 @@err = json.Unmarshal(data, &p)}if err != nil {return nil, fmt.Errorf("parse persona %s: %w", source, err)[NIT] The comment
// go-yaml v1.16.0+ has built-in protection against deeply nested structuresis misleading/incorrect forgopkg.in/yaml.v3. It appears to have been written forgoccy/go-yamland copied over without updating. This should either be removed or corrected to accurately describegopkg.in/yaml.v3's actual behavior.@@ -184,12 +277,25 @@ func TestLoadPersonaValidation(t *testing.T) {}[MAJOR] The design checklist explicitly requires "Test for deeply nested YAML rejection" (item 7 in the completion checklist), but no such test exists. This is especially important given that the security rationale for this PR includes protection against AIKIDO-2024-10486 (deeply nested YAML DoS). Without this test, the security property is unverified.
Original reviewSuperseded — see current review for up-to-date findings.
Previous findings (commit
7898dd93)Gpt Review
Summary
Solid implementation of YAML support with clean extension-based detection, backward-compatible JSON handling, embedded YAML personas, and comprehensive tests. CI passed and the dependency adheres to the repository allowlist.
Findings
docs/DESIGN-57-yaml-persona.mdreview/persona.goREADME.mddocs/DESIGN-57-yaml-persona.mdreview/persona.goRecommendation
APPROVE — The code changes are correct and idiomatic: YAML parsing uses an approved dependency, format detection is case-insensitive by extension, built-in personas are embedded as YAML and loaded first with JSON fallback for compatibility, and tests thoroughly cover YAML and JSON cases including comments and multiline strings. CI passed. Recommend merging after minor documentation cleanups: update the design doc to reflect the actual YAML library (gopkg.in/yaml.v3) and remove or adjust claims about depth protection, fix the README feature that still claims zero dependencies, and optionally sort the result of ListBuiltinPersonas for deterministic ordering. These are non-blocking improvements.
Review by gpt
Evaluated against
7898dd93@@ -0,0 +1,93 @@# Design: YAML Support for Persona Files (#57)[MINOR] Design doc references github.com/goccy/go-yaml and claims built-in nesting depth protection, but the code uses gopkg.in/yaml.v3 and does not set an explicit depth limit. Align the design doc with the actual dependency and behavior, or add explicit protection if required.
@@ -0,0 +8,4 @@- Backwards compatibility: existing JSON personas must continue to work- Security: protect against DoS via deeply nested YAML (AIKIDO-2024-10486)- Consistency: use `.yaml` extension (not `.yml`)[NIT] The design constraint says to use '.yaml' (not '.yml') for consistency, but the implementation and README support both. Clarify the recommendation vs. support to avoid confusion.
@@ -52,3 +64,3 @@}return parsePersona(data, "builtin:"+name)return parsePersona(data, "builtin:"+jsonFile)}[NIT] ListBuiltinPersonas returns names in non-deterministic order (map iteration). Consider sorting the slice before returning for determinism, which can help with UX and potential downstream consumers.
@@ -71,0 +85,4 @@personaName = strings.TrimSuffix(name, ".yaml")case strings.HasSuffix(name, ".yml"):personaName = strings.TrimSuffix(name, ".yml")case strings.HasSuffix(name, ".json"):[NIT] The comment in parsePersona mentions 'go-yaml v1.16.0+ has built-in protection against deeply nested structures' which refers to a different library (goccy/go-yaml). Update the comment to reflect gopkg.in/yaml.v3 or remove the claim.
Security Review
Summary
YAML support is implemented cleanly and CI passes, but loading untrusted YAML/JSON persona files uses unbounded parsing and reading. This creates a denial-of-service vector via deeply nested or oversized YAML content, especially since persona files can come from PR contents in CI.
Findings
review/persona.goreview/persona.godocs/DESIGN-57-yaml-persona.mdRecommendation
REQUEST_CHANGES — To mitigate DoS vectors, add hard limits and safer parsing for persona files. Concretely:
These changes will reduce the risk that a crafted persona file in a PR causes resource exhaustion in CI while retaining the new YAML functionality.
Review by security
Evaluated against
7898dd93@@ -0,0 +20,4 @@5. **Update embed directive** to include both formats### File Extension Detection[NIT] Design doc references github.com/goccy/go-yaml depth protections, but the implementation uses gopkg.in/yaml.v3. This mismatch may give a false sense of protection; yaml.v3 has no explicit caller-configurable limits in this code.
@@ -36,2 +37,4 @@// LoadPersona loads a persona from a JSON or YAML file path.// Format is detected by file extension: .yaml/.yml for YAML, .json or other for JSON.func LoadPersona(path string) (*Persona, error) {data, err := os.ReadFile(path)[MINOR] os.ReadFile reads the entire persona file into memory without a size cap. A maliciously large persona file could cause high memory usage on the CI runner (resource exhaustion).
@@ -79,0 +111,4 @@var err errorif isYAML {// go-yaml v1.16.0+ has built-in protection against deeply nested structureserr = yaml.Unmarshal(data, &p)[MAJOR] Unbounded YAML deserialization with yaml.Unmarshal on potentially untrusted persona files can allow resource exhaustion (e.g., deeply nested structures or alias expansion bombs). There is no explicit limit on nesting, aliases, or input size, contrary to the comment suggesting built-in protection.
Review Findings Addressed
Pushed commit
c3e8f0fto address all review findings.MAJOR Fixes
Removed false security claim — The comment said
go-yaml v1.16.0+has built-in depth protection, butgopkg.in/yaml.v3does not. Removed the misleading comment.Added explicit YAML depth limiting — Implemented
unmarshalYAMLWithDepthLimit()using theyaml.NodeAPI to walk the parse tree and reject documents deeper than 20 levels.Added file size limit —
LoadPersona()now rejects files larger than 64KB viaos.Stat()before reading.Added test for deeply nested YAML rejection —
TestYAMLDeeplyNestedRejectiongenerates a 25-level nested structure and verifies it's rejected.MINOR Fixes
Deterministic persona listing — Added
sort.Strings(names)toListBuiltinPersonas().Updated design doc — Changed library reference from
goccy/go-yamltogopkg.in/yaml.v3with accurate description of depth protection approach.Updated README — Changed "Zero dependencies: Go stdlib only" to "Minimal dependencies: Go stdlib +
gopkg.in/yaml.v3only".Added tests —
TestYAMLFileSizeLimitandTestListBuiltinPersonasSortedOrderfor coverage.All tests pass (
go test ./...), no vet warnings (go vet ./...).Sonnet Review
Summary
This PR correctly adds YAML support for persona files using the approved
gopkg.in/yaml.v3dependency. The implementation is solid: it uses the two-pass decode approach (yaml.Node for depth checking, then struct decode), handles backwards compatibility with JSON, and has comprehensive test coverage. The code follows project patterns and conventions closely.Findings
review/persona.gounmarshalYAMLWithDepthLimitfunction usesinterface{}as the parameter type foroutinstead ofany. The project targets the latest stable Go release, andanyis the idiomatic alias since Go 1.18. This is inconsistent with the rest of the codebase convention.review/persona.goyaml.NewDecoderis used to decode a single document but does not check for a second call toDecodereturningio.EOFto confirm there's only one document. For untrusted user-supplied YAML files, silently ignoring additional documents is acceptable (they won't affect the parsed struct), but it's worth noting that multi-document YAML files will have their additional documents silently ignored rather than surfacing an error. This is a minor UX issue, not a correctness bug.docs/DESIGN-57-yaml-persona.mdparseYAMLcode snippet in the 'YAML Parsing with Depth Protection' section shows a naiveyaml.Unmarshalwithout depth checking, but the actual implementation correctly uses the two-pass Node approach. The doc snippet is stale/inconsistent with the implementation. The paragraph below it explains the correct approach, but the code block above it is misleading. This is documentation-only and doesn't affect behavior, but could confuse future contributors.review/persona.goListBuiltinPersonas, the map updateif !seen[personaName] { seen[personaName] = true }can be simplified to justseen[personaName] = true— setting totruewhen alreadytrueis a no-op. The guard is unnecessary.review/persona.goLoadBuiltinPersona, the JSON fallback path reassignsdata, err = embeddedPersonas.ReadFile(...)but the originalerrfrom the YAML attempt is already discarded by theif err == nil { return ... }pattern. This is fine and correct, but worth noting that the error variable from the YAML attempt is silently dropped. Since the YAML error is expected (file doesn't exist), this is intentional and acceptable.Recommendation
APPROVE — The implementation is correct, well-tested, and follows the repository's conventions (uses only approved dependencies, wraps errors with context, table-driven tests). The dependency
gopkg.in/yaml.v3is explicitly in the approved allowlist. The depth-limiting via two-pass yaml.Node decode is a sound security measure. CI passes. Approve with the minor suggestions above — none are blocking. The stale code snippet in the design doc is the most worth fixing but it's a documentation artifact, not production code.Review by sonnet
Evaluated against
c3e8f0f2@@ -0,0 +43,4 @@}if err := validatePersona(&p, source); err != nil {return nil, err}[MINOR] The design document's
parseYAMLcode snippet in the 'YAML Parsing with Depth Protection' section shows a naiveyaml.Unmarshalwithout depth checking, but the actual implementation correctly uses the two-pass Node approach. The doc snippet is stale/inconsistent with the implementation. The paragraph below it explains the correct approach, but the code block above it is misleading. This is documentation-only and doesn't affect behavior, but could confuse future contributors.@@ -43,39 +64,76 @@ func LoadPersona(path string) (*Persona, error) {[NIT] In
LoadBuiltinPersona, the JSON fallback path reassignsdata, err = embeddedPersonas.ReadFile(...)but the originalerrfrom the YAML attempt is already discarded by theif err == nil { return ... }pattern. This is fine and correct, but worth noting that the error variable from the YAML attempt is silently dropped. Since the YAML error is expected (file doesn't exist), this is intentional and acceptable.@@ -71,0 +106,4 @@case strings.HasSuffix(name, ".json"):personaName = strings.TrimSuffix(name, ".json")default:continue[NIT] In
ListBuiltinPersonas, the map updateif !seen[personaName] { seen[personaName] = true }can be simplified to justseen[personaName] = true— setting totruewhen alreadytrueis a no-op. The guard is unnecessary.@@ -87,0 +150,4 @@if err := dec.Decode(&node); err != nil {return err}[MINOR] The
unmarshalYAMLWithDepthLimitfunction usesinterface{}as the parameter type foroutinstead ofany. The project targets the latest stable Go release, andanyis the idiomatic alias since Go 1.18. This is inconsistent with the rest of the codebase convention.@@ -87,0 +158,4 @@return node.Decode(out)}// checkYAMLDepth recursively checks that YAML nodes don't exceed the depth limit.[MINOR] The decoder created with
yaml.NewDecoderis used to decode a single document but does not check for a second call toDecodereturningio.EOFto confirm there's only one document. For untrusted user-supplied YAML files, silently ignoring additional documents is acceptable (they won't affect the parsed struct), but it's worth noting that multi-document YAML files will have their additional documents silently ignored rather than surfacing an error. This is a minor UX issue, not a correctness bug.Security Review
Summary
YAML support is added with sensible defenses: a 64KB file-size cap, depth-limited YAML decoding, and strict format detection by extension. CI passed and no exploitable vulnerabilities are evident, though there are minor hardening opportunities around file handling.
Findings
review/persona.goreview/persona.goRecommendation
APPROVE — Overall, the changes are sound and improve functionality without introducing clear vulnerabilities. For defense-in-depth against resource exhaustion and hangs, re-validate the size after reading (e.g., check len(data) <= MaxPersonaFileSize) and ensure the file is regular (info.Mode().IsRegular()) before reading. The YAML depth check and 64KB limit are good safeguards; these additional checks will further reduce DoS risk from malicious or unusual files. With these minor hardening tweaks considered, the PR is good to merge.
Review by security
Evaluated against
c3e8f0f2@@ -43,39 +64,76 @@ func LoadPersona(path string) (*Persona, error) {// LoadBuiltinPersona loads a built-in persona by name.[MINOR] Missing regular file check: LoadPersona does not verify that the path refers to a regular file. Reading special files (FIFOs, devices) could block or cause unexpected behavior. Consider rejecting non-regular files (info.Mode().IsRegular()).
@@ -48,1 +69,3 @@data, err := embeddedPersonas.ReadFile("personas/" + filename) // embed.FS paths use forward slashes per io/fs spec// Try YAML first (preferred format)yamlFile := name + ".yaml"data, err := embeddedPersonas.ReadFile("personas/" + yamlFile)[MINOR] Potential TOCTOU/resource exhaustion risk: file size is checked via os.Stat before reading, but not re-validated after os.ReadFile. An attacker with control over the path could swap the file between Stat and Read, bypassing the size cap and causing large in-memory reads.
Gpt Review
Summary
Solid addition of YAML support with careful depth-limited parsing, size limits, extension detection, and comprehensive tests. Documentation and embedded personas were updated consistently, and dependency usage respects the repository’s allowlist.
Findings
review/persona.goreview/persona.godocs/DESIGN-57-yaml-persona.mdgopkg.in/yaml.v3 v3.0.1dependency used. Consider updating the doc to avoid confusion.Recommendation
APPROVE — The implementation is correct, idiomatic, and well-tested. Approve and consider small follow-ups: (1) enforce the file size limit after reading by checking len(data) to close a TOCTOU gap, and (2) enhance YAML depth protection by considering alias nodes during traversal. Optionally, update the design document to reflect the actual yaml.v3 dependency version.
Review by gpt
Evaluated against
c3e8f0f2@@ -0,0 +9,4 @@- Backwards compatibility: existing JSON personas must continue to work- Security: protect against DoS via deeply nested YAML (AIKIDO-2024-10486)- Consistency: use `.yaml` extension (not `.yml`)- Library: use `gopkg.in/yaml.v3` (approved in CONVENTIONS.md) with explicit depth limiting[NIT] The design doc checklist references adding a "go-yaml" dependency at v1.16.0+, which doesn't align with the actual
gopkg.in/yaml.v3 v3.0.1dependency used. Consider updating the doc to avoid confusion.@@ -33,1 +42,4 @@Major string `json:"major" yaml:"major"`Minor string `json:"minor" yaml:"minor"`Nit string `json:"nit" yaml:"nit"`}[MINOR] LoadPersona enforces the size limit using os.Stat before reading, but does not re-check len(data) after os.ReadFile. A TOCTOU window could allow reading files larger than MaxPersonaFileSize if the file grows between stat and read.
@@ -87,0 +148,4 @@var node yaml.Nodedec := yaml.NewDecoder(bytes.NewReader(data))if err := dec.Decode(&node); err != nil {return err[MINOR] YAML depth check traverses node.Content but ignores Alias nodes (node.Alias). Malicious YAML could leverage anchors/aliases to create effective deep structures without increasing Content depth. Consider handling Alias nodes explicitly.
Self-review: PASS
Analysis
Coherence ✅
Fit ✅
parsePersona,validatePersona, error wrapping with%w)Solution ✅
Completeness ✅
Detailed Review
persona.go:
MaxPersonaFileSize(64 KB) andMaxYAMLDepth(20) are sensible limitsunmarshalYAMLWithDepthLimitcorrectly decodes toyaml.Node, checks depth, then decodes to structstrings.ToLoweris correctLoadBuiltinPersonatries YAML first, falls back to JSON—good migration pathListBuiltinPersonasnow sorted and deduplicated—clean improvementTests:
TestYAMLDeeplyNestedRejectionverifies DoS protectionTestYAMLFileSizeLimitverifies size protectionTestLoadPersonaCaseInsensitiveExtensioncovers Windows/macOS scenariosNo issues found—ready for human review.
c3e8f0freviewReview Findings Addressed (
c3e8f0f→6035afe)Addressed all MINOR findings from the reviews against
c3e8f0f:Code Changes (review/persona.go)
interface{} → any — Updated
unmarshalYAMLWithDepthLimitparameter type frominterface{}toany(idiomatic since Go 1.18).Multi-document YAML behavior documented — Added comment explaining that multi-document YAML files have additional documents silently ignored (acceptable for persona file use case).
Regular file check — Added
info.Mode().IsRegular()check afteros.Statto reject non-regular files (FIFOs, devices, etc.).TOCTOU size re-validation — Added
len(data) > MaxPersonaFileSizecheck afteros.ReadFileto defend against race conditions where file grows between stat and read.Alias node handling in depth check — Updated
checkYAMLDepthto handle alias nodes by followingnode.Aliaspointer, preventing alias-based deep structure bypass.Documentation (docs/DESIGN-57-yaml-persona.md)
yaml.Unmarshalexample with actual two-passyaml.Nodeimplementation including depth checking and alias handling.All tests pass (
go test ./...), no vet issues (go vet ./...).Sonnet Review
Summary
This PR correctly adds YAML support for persona files using the approved
gopkg.in/yaml.v3dependency. The implementation is well-structured with appropriate security mitigations (depth limiting, file size caps, TOCTOU protection), comprehensive tests, and clean backwards-compatible fallback to JSON. CI passes.Findings
review/persona.gounmarshalYAMLWithDepthLimitfunction silently ignores multi-document YAML files (only parses the first document). While the comment acknowledges this, it could be a footgun: a user who accidentally writes---in their persona file will get no error, just silent truncation. Consider callingdec.Decodea second time and returning an error if a second document is found, to give users a clearer signal.review/persona.gocheckYAMLDepthdoes not count the alias node's own depth level before recursing into the alias target. If an alias node appears near the depth limit, the target's content is checked starting from the samedepthvalue as the alias itself (notdepth+1). This means a deeply aliased structure could potentially bypass the limit by 1 level. While the impact is minimal given the depth=20 limit, incrementing depth before recursing into the alias target would be more consistent.docs/DESIGN-57-yaml-persona.mdreview/persona.goListBuiltinPersonas,if !seen[personaName] { seen[personaName] = true }can be simplified toseen[personaName] = true— the guard is unnecessary since setting an already-true bool to true is a no-op.Recommendation
APPROVE — The implementation is solid and production-ready. The MINOR findings are low-risk edge cases that don't affect current functionality. The alias depth counting off-by-one is worth a quick look but given MaxYAMLDepth=20 is generous, it poses no practical risk. The multi-document silencing is acknowledged in the code comment. Approve as-is; the findings can be addressed in a follow-up if desired.
Review by sonnet
Evaluated against
6035afee@@ -0,0 +52,4 @@}// Handle alias nodes by following the Alias pointerif node.Kind == yaml.AliasNode && node.Alias != nil {return checkYAMLDepth(node.Alias, depth, maxDepth)[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.
@@ -69,2 +107,2 @@if strings.HasSuffix(name, ".json") {names = append(names, strings.TrimSuffix(name, ".json"))// Strip extension to get persona namevar personaName string[NIT] In
ListBuiltinPersonas,if !seen[personaName] { seen[personaName] = true }can be simplified toseen[personaName] = true— the guard is unnecessary since setting an already-true bool to true is a no-op.@@ -87,0 +158,4 @@func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {var node yaml.Nodedec := yaml.NewDecoder(bytes.NewReader(data))if err := dec.Decode(&node); err != nil {[MINOR] The
unmarshalYAMLWithDepthLimitfunction silently ignores multi-document YAML files (only parses the first document). While the comment acknowledges this, it could be a footgun: a user who accidentally writes---in their persona file will get no error, just silent truncation. Consider callingdec.Decodea second time and returning an error if a second document is found, to give users a clearer signal.@@ -87,0 +173,4 @@// 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)[MINOR] The alias-following logic in
checkYAMLDepthdoes not count the alias node's own depth level before recursing into the alias target. If an alias node appears near the depth limit, the target's content is checked starting from the samedepthvalue as the alias itself (notdepth+1). This means a deeply aliased structure could potentially bypass the limit by 1 level. While the impact is minimal given the depth=20 limit, incrementing depth before recursing into the alias target would be more consistent.Gpt Review
Summary
Solid addition of YAML persona support with careful size limits, depth checks, and extensive tests. Implementation aligns with repository conventions and CI passed.
Findings
review/persona.goreview/persona.goreview/persona.goreview/persona.goRecommendation
APPROVE — The implementation is well-structured, adheres to the dependency allowlist, and provides robust tests and documentation. Approve as-is. For hardening, consider adding cycle protection when following YAML aliases, and enable strict field checking (KnownFields) to catch configuration typos. Also align the design doc and comments with the current embed strategy or embed both formats if you plan to retain JSON fallbacks.
Review by gpt
Evaluated against
6035afee@@ -10,3 +14,3 @@)//go:embed personas/*.json//go:embed personas/*.yaml[NIT] Only YAML files are embedded (//go:embed personas/*.yaml) while the design doc mentions embedding both formats; consider updating the comment/design doc or embedding both if JSON files may ever return.
@@ -44,3 +73,4 @@// LoadBuiltinPersona loads a built-in persona by name.// Returns an error if the persona doesn't exist.// Built-in personas are stored in YAML format.func LoadBuiltinPersona(name string) (*Persona, error) {[NIT] LoadBuiltinPersona falls back to reading JSON from embedded FS, but JSON personas are no longer embedded; clarify the comment or remove the fallback if not needed.
@@ -79,2 +143,4 @@}if err != nil {return nil, fmt.Errorf("parse persona %s: %w", source, err)}[MINOR] YAML decoder does not enable KnownFields/strict mode, so unknown keys in persona YAML are silently ignored. Enabling strict field checking would help catch typos (e.g., dec.KnownFields(true)) before validation.
@@ -87,0 +162,4 @@return err}if err := checkYAMLDepth(&node, 0, maxDepth); err != nil {[MINOR] checkYAMLDepth follows alias pointers without guarding against potential alias cycles; consider tracking visited nodes or limiting alias dereference to avoid possible infinite recursion on pathological inputs.
Security Review
Summary
Adds YAML support with size and depth limits and keeps JSON compatibility. However, the YAML depth check can be bypassed or abused via alias cycles, creating a denial‑of‑service risk (infinite recursion/stack exhaustion) when parsing untrusted persona files.
Findings
review/persona.goreview/persona.goRecommendation
REQUEST_CHANGES — The YAML parsing implementation is generally solid (size limit, file type checks, and a max depth). However, the current depth checker can be defeated or abused through alias cycles, creating a denial-of-service vector. Please update checkYAMLDepth to track visited nodes and short-circuit on cycles, e.g., by maintaining a map[*yaml.Node]bool visited passed through recursion. Alternatively, impose a global cap on total node visits to bound work. Also consider incrementing depth when traversing an alias used as a child value to align depth counting with non-alias nodes. Optionally, you could enable additional hardening such as yaml.Decoder.KnownFields(true) to reject unexpected fields (not a security requirement). After addressing the alias cycle recursion risk, the YAML support can be approved.
Review by security
Evaluated against
6035afee@@ -87,0 +156,4 @@// 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[MAJOR] The recursive YAML depth checker does not detect alias/anchor cycles. If a YAML document contains cyclic aliases (e.g., an alias ultimately pointing back to an anchored node that references the alias), checkYAMLDepth can recurse indefinitely, leading to stack exhaustion or a hang (DoS). Add cycle detection (visited set of *yaml.Node) or a total node visitation cap to prevent infinite recursion.
@@ -87,0 +161,4 @@if err := dec.Decode(&node); err != nil {return err}[MINOR] When encountering an alias node, the depth check calls checkYAMLDepth on the alias target without incrementing depth, potentially undercounting nesting by one compared to non-alias children. While limited in practical bypass (one level), this slightly weakens the intended MaxYAMLDepth enforcement. Consider passing depth+1 when following an alias used as a value.
Addressing gpt-review-bot MINOR #2 — YAML strict field checking
Fixed in commit
4fed59a.Enabled
KnownFields(true)on the YAML decoder to reject unknown fields. This catches typos likefocussoridentiyin persona files.Implementation notes:
yaml.Node.Decode()does not support theKnownFieldsoption, the fix uses a two-pass approach:yaml.Nodeto check depth limits (existing security measure)All tests pass. The fix adds minimal overhead (one extra decode pass) but provides meaningful protection against configuration typos.
Sonnet Review
Summary
This PR adds YAML support for persona files using the already-approved
gopkg.in/yaml.v3dependency. The implementation is well-designed with proper security controls (size limits, depth limiting, strict field checking), good backwards compatibility with JSON, and comprehensive test coverage. CI passes.Findings
review/persona.gogopkg.in/yaml.v3ever adds KnownFields support to node.Decode(), this could be simplified — but for now this is the correct workaround and is well-documented.review/persona.goreturn checkYAMLDepth(node.Alias, depth, maxDepth)— i.e., the alias itself doesn't add to the depth. This is intentional per the comment, but YAML aliases can be used to create large fan-out structures. An alias to a deeply nested structure effectively doubles the depth impact without incrementing depth for the alias node itself. For the persona use case with MaxYAMLDepth=20 this is acceptable, but worth noting the design tradeoff.review/persona.goListBuiltinPersonas, theseenmap ismap[string]booland the code checksif !seen[personaName]before setting it totrue. The!seen[...]check is redundant since setting it totrueagain would be a no-op. The code works correctly but the guard condition (if !seen[personaName]) adds noise — it could just beseen[personaName] = trueunconditionally.docs/DESIGN-57-yaml-persona.mdreview/persona.goLoadBuiltinPersonasays 'Try YAML first (preferred format)' but then falls back to JSON. Since the embed directive is now//go:embed personas/*.yamlonly (JSON files were deleted), the JSON fallback inLoadBuiltinPersonais dead code for built-in personas. It's harmless and provides forwards-compatibility for the unlikely case someone adds a.jsonbuiltin later, but could be simplified.Recommendation
APPROVE — Approve this PR. The implementation is solid: it uses the project-approved
gopkg.in/yaml.v3library, adds proper security controls (TOCTOU-resistant size limits, explicit depth checking, strict field rejection), maintains backwards compatibility with JSON, and is thoroughly tested (20+ new test cases covering multiline strings, comments, case-insensitive extensions, unknown fields, depth limits, and size limits). The minor findings are all non-blocking: the two-pass decode is the correct approach given the library's limitations, the alias depth behavior is acceptable for this use case, and the dead JSON fallback code is harmless. The design document discrepancy is documentation-only.Review by sonnet
Evaluated against
4fed59ac@@ -0,0 +44,4 @@return err}return node.Decode(out)}[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.@@ -37,0 +57,4 @@}if info.Size() > MaxPersonaFileSize {return nil, fmt.Errorf("persona file %s exceeds maximum size (%d bytes)", path, MaxPersonaFileSize)}[NIT] The comment on
LoadBuiltinPersonasays 'Try YAML first (preferred format)' but then falls back to JSON. Since the embed directive is now//go:embed personas/*.yamlonly (JSON files were deleted), the JSON fallback inLoadBuiltinPersonais dead code for built-in personas. It's harmless and provides forwards-compatibility for the unlikely case someone adds a.jsonbuiltin later, but could be simplified.@@ -55,3 +93,3 @@// ListBuiltinPersonas returns the names of all built-in personas.// ListBuiltinPersonas returns the names of all built-in personas in sorted order.// Returns an empty slice if the embedded directory cannot be read.[NIT] In
ListBuiltinPersonas, theseenmap ismap[string]booland the code checksif !seen[personaName]before setting it totrue. The!seen[...]check is redundant since setting it totrueagain would be a no-op. The code works correctly but the guard condition (if !seen[personaName]) adds noise — it could just beseen[personaName] = trueunconditionally.@@ -84,6 +150,53 @@ func parsePersona(data []byte, source string) (*Persona, error) {return &p, nil}// unmarshalYAMLWithDepthLimit unmarshals YAML data with explicit depth limiting[MINOR] The two-pass decode approach (first into yaml.Node for depth check, then strict decode from raw bytes) is necessary because KnownFields doesn't work on yaml.Node.Decode(), but this means the input bytes are parsed twice. The comment explains this correctly. A minor concern: if
gopkg.in/yaml.v3ever adds KnownFields support to node.Decode(), this could be simplified — but for now this is the correct workaround and is well-documented.@@ -87,0 +174,4 @@// We must re-decode from the original data because yaml.Node.Decode() doesn't// support the KnownFields option.strictDec := yaml.NewDecoder(bytes.NewReader(data))strictDec.KnownFields(true)[MINOR] The alias depth check uses
return checkYAMLDepth(node.Alias, depth, maxDepth)— i.e., the alias itself doesn't add to the depth. This is intentional per the comment, but YAML aliases can be used to create large fan-out structures. An alias to a deeply nested structure effectively doubles the depth impact without incrementing depth for the alias node itself. For the persona use case with MaxYAMLDepth=20 this is acceptable, but worth noting the design tradeoff.Gpt Review
Summary
Solid addition of YAML support with careful depth limiting, strict field checks, file size caps, and comprehensive tests. Dependency choice (gopkg.in/yaml.v3) aligns with the repo’s allowlist and README; built-in personas migrated cleanly to YAML.
Findings
review/persona.goreview/persona.goreview/persona.godocs/DESIGN-57-yaml-persona.mdreview/persona.goRecommendation
APPROVE — The implementation is correct, idiomatic, and adheres to repository constraints. YAML parsing uses strict KnownFields and an explicit depth limit, file size limits guard against DoS, and tests cover key scenarios including multi-line strings, comments, case-insensitive extensions, unknown fields, deep nesting, and size limits. Minor polish items include clarifying an error message, optionally rejecting multi-document YAML instead of ignoring extras, aligning the design doc with the final implementation, and considering removal of the now-unused JSON fallback for embedded personas. None of these are blocking. Approve as-is.
Review by gpt
Evaluated against
4fed59ac@@ -0,0 +1,108 @@# Design: YAML Support for Persona Files (#57)[NIT] Design doc references adding an embed directive for both formats and mentions go-yaml versioning, but the implementation embeds only *.yaml and uses gopkg.in/yaml.v3 v3.0.1. Consider updating the design doc to reflect the final implementation.
@@ -1,81 +1,147 @@package review[NIT] LoadPersona wraps os.Stat errors with message 'read persona file ...', which can be slightly misleading; consider using a 'stat persona file' prefix for the stat error path to improve specificity.
[NIT] unmarshalYAMLWithDepthLimit accepts multi-document YAML but silently ignores subsequent documents. Consider rejecting multi-document inputs to avoid ambiguity or add a comment in README clarifying this behavior.
[NIT] LoadBuiltinPersona falls back to JSON for embedded personas, but only *.yaml files are embedded now; the JSON fallback is effectively dead code. Optional: either embed both formats or remove the fallback to simplify.
[NIT] ListBuiltinPersonas filters by case-sensitive suffixes; while embedded filenames are controlled and lowercase now, using a case-insensitive check would be slightly more robust if naming conventions change.
Security Review
Summary
Overall, the YAML support is implemented thoughtfully with size limits, strict field checking, and case-insensitive extension detection. However, there is a potential denial-of-service vector in the YAML depth checker due to unguarded recursion over aliases, which can lead to infinite recursion on cyclical aliases.
Findings
review/persona.goreview/persona.goRecommendation
REQUEST_CHANGES — Address the potential DoS in checkYAMLDepth by adding cycle detection when following alias nodes. Pass a visited set (e.g., map[*yaml.Node]bool) through the recursion and skip nodes already seen to prevent infinite recursion on cyclical aliases. For example:
func checkYAMLDepth(node *yaml.Node, depth, maxDepth int, seen map[*yaml.Node]struct{}) error {
if depth > maxDepth { return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth) }
if _, ok := seen[node]; ok { return nil }
seen[node] = struct{}{}
if node.Kind == yaml.AliasNode && node.Alias != nil {
return checkYAMLDepth(node.Alias, depth, maxDepth, seen)
}
for _, child := range node.Content {
if err := checkYAMLDepth(child, depth+1, maxDepth, seen); err != nil { return err }
}
return nil
}
Update the call sites accordingly (initialize seen := make(map[*yaml.Node]struct{})). Optionally, consider additional defenses: (1) limit the total number of nodes visited and/or alias references traversed to mitigate other resource exhaustion patterns; (2) keep the current 64KB cap and MaxYAMLDepth=20, but document them as security limits and consider lowering them if feasible. With these changes, the YAML parsing logic will be more robust against crafted inputs while retaining the current strict-field and size protections.
Review by security
Evaluated against
4fed59ac@@ -87,0 +161,4 @@// This prevents stack exhaustion before we attempt to decode into structs.var node yaml.Nodedec := yaml.NewDecoder(bytes.NewReader(data))if err := dec.Decode(&node); err != nil {[MINOR] Depth enforcement occurs after decoding the full YAML into a yaml.Node. While the 64KB file-size cap mitigates risk, an attacker could still supply deeply nested but small YAML to stress the decoder before the depth check. Consider additional safeguards (e.g., limiting total nodes visited or using a visited set, or further reducing allowed size/depth) for defense in depth.
@@ -87,0 +186,4 @@}// 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 {[MAJOR] checkYAMLDepth follows yaml aliases recursively without cycle detection. A crafted YAML with cyclical/self-referential aliases can cause infinite recursion and a panic, enabling a denial-of-service against the process loading persona files.
Address security review findings: MAJOR: Add cycle detection to checkYAMLDepth using a visited set (seen map[*yaml.Node]struct{}) to prevent infinite recursion from crafted YAML with self-referential aliases. MINOR fixes: - Add MaxYAMLNodes (1000) limit as defense-in-depth against wide-but-shallow structures that bypass depth limits - Increment depth when following alias targets (was incorrectly passing same depth, allowing alias chains to bypass depth limit) - Reject multi-document YAML files instead of silently ignoring additional documents (prevents confusing silent data loss) Tests added: - TestYAMLAliasCycleDetection: Direct test of cycle detection logic - TestYAMLMultiDocumentRejection: Verifies multi-doc files rejected - TestYAMLNodeCountLimit: Verifies wide structures are rejected - TestCheckYAMLDepthCycleDetectionDirect: Unit test with artificial cycleReview Findings Addressed
Commit
26f326caddresses all security and code review findings:MAJOR (security-review-bot)
Alias cycle detection - Added
seen map[*yaml.Node]struct{}parameter tocheckYAMLDepthto track visited nodes and prevent infinite recursion from crafted YAML with self-referential aliases. When a node is encountered a second time, we skip validation (already done) instead of recursing infinitely.MINOR Findings
Node count limit (security-review-bot) - Added
MaxYAMLNodes = 1000constant and tracking to limit total nodes visited. This prevents DoS via wide-but-shallow structures that bypass depth limits.Alias depth increment (sonnet-review-bot) - Fixed: Now increments depth when following alias targets. Previously passed the same depth, which would allow alias chains to bypass the depth limit.
Multi-document rejection (sonnet-review-bot) - Changed from silently ignoring additional documents to returning an error. Prevents confusing behavior where users think their changes take effect but they are actually in a second document.
Alias cycle detection (gpt-review-bot) - Same as MAJOR above.
Tests Added
TestYAMLAliasCycleDetection: Direct test of cycle detection with artificial node cycleTestYAMLMultiDocumentRejection: Verifies multi-document YAML files are rejectedTestYAMLNodeCountLimit: Verifies wide structures exceeding 1000 nodes are rejectedTestCheckYAMLDepthCycleDetectionDirect: Unit test creating artificial cycle to verify seen map worksAll tests pass:
go test ./...✓Sonnet Review
Summary
This PR correctly adds YAML support for persona files using the already-approved
gopkg.in/yaml.v3dependency. The implementation is well-structured with appropriate security hardening (depth limits, node count limits, cycle detection, multi-document rejection, strict field checking, file size limits). The tests are comprehensive and follow established patterns.Findings
review/persona.goreview/persona.godec.Decode(&extra)) re-uses the same decoder after the firstdec.Decode(&node)call. If the first document exhausted the decoder but there's trailing non-document content (e.g., a trailing...end-of-document marker), the behavior depends on go-yaml internals. This is likely fine in practice, but the comment could clarify that this relies on go-yaml's decoder advancing past the first document.review/persona.goListBuiltinPersonas, the map population pattern is slightly redundant:if !seen[personaName] { seen[personaName] = true }. Since we only ever set it totrue, the guard!seen[personaName]is equivalent to justseen[personaName] = true(maps return zero-value false for missing keys, and overwriting with true is idempotent). The condition adds no value. Could simplify toseen[personaName] = true.review/persona_test.goTestLoadPersonaFromJSONFilehas a blank line added inside the JSON content string ("focus": ["testing", "validation"],\n\n"ignore":...). While valid JSON, the blank line is unnecessary and looks like an accidental edit.docs/DESIGN-57-yaml-persona.mdRecommendation
APPROVE — Approve. The implementation is solid — it uses the approved
gopkg.in/yaml.v3dependency correctly, adds meaningful security hardening that goes beyond the library's defaults (depth limits, node counts, cycle detection, strict field validation, file size limits, multi-document rejection), maintains backwards compatibility with JSON, and has thorough test coverage. The minor findings are cosmetic or low-impact and don't warrant blocking the PR.Review by sonnet
Evaluated against
26f326cf@@ -0,0 +73,4 @@| Error | Handling ||-------|----------|| Invalid YAML syntax | Return parse error with source file |[NIT] The design doc's error table says 'Deeply nested YAML | Library rejects (v1.16.0+ fix)' but the actual implementation uses explicit application-level depth checking (not library-level). The table is inaccurate. Minor doc inconsistency.
@@ -64,4 +106,4 @@for _, e := range entries {if e.IsDir() {continue}[NIT] In
ListBuiltinPersonas, the map population pattern is slightly redundant:if !seen[personaName] { seen[personaName] = true }. Since we only ever set it totrue, the guard!seen[personaName]is equivalent to justseen[personaName] = true(maps return zero-value false for missing keys, and overwriting with true is idempotent). The condition adds no value. Could simplify toseen[personaName] = true.@@ -87,0 +168,4 @@}// Reject multi-document YAML files - silently ignoring additional documents// could lead to confusing behavior where users think their changes take effect.[MINOR] The multi-document rejection check (
dec.Decode(&extra)) re-uses the same decoder after the firstdec.Decode(&node)call. If the first document exhausted the decoder but there's trailing non-document content (e.g., a trailing...end-of-document marker), the behavior depends on go-yaml internals. This is likely fine in practice, but the comment could clarify that this relies on go-yaml's decoder advancing past the first document.@@ -87,0 +180,4 @@}// Second pass: decode with strict field checking enabled.// KnownFields(true) rejects unknown keys, catching typos like "focuss" or "identiy".[MINOR] The two-pass decode approach (first into yaml.Node for depth checking, then a fresh decoder with KnownFields) re-parses the raw bytes a second time. This means a malicious YAML that causes the first-pass decoder to consume significant CPU (e.g., extremely long scalar strings) gets two chances to do so. The depth/node count limits reduce but don't fully eliminate this risk. A minor concern since file size is bounded at 64KB, but worth noting.
@@ -96,6 +176,7 @@ func TestLoadPersonaFromJSONFile(t *testing.T) {"display_name": "Test Persona",[NIT] The test
TestLoadPersonaFromJSONFilehas a blank line added inside the JSON content string ("focus": ["testing", "validation"],\n\n"ignore":...). While valid JSON, the blank line is unnecessary and looks like an accidental edit.Security Review
Summary
The YAML persona support is implemented with solid security hardening: strict field checks, depth/node limits, single-document enforcement, and file size/regular-file validations. Dependency usage aligns with the approved allowlist and CI passed.
Findings
review/persona.goRecommendation
APPROVE — Overall, the changes add YAML support with robust defenses against unsafe deserialization and resource exhaustion: multi-document YAML is rejected, nesting depth and node count are limited, alias cycles are guarded, and persona files are size-limited with TOCTOU protection. The new dependency gopkg.in/yaml.v3 is on the approved list. As a defense-in-depth improvement, update JSON parsing to use a Decoder with DisallowUnknownFields to mirror the strictness applied to YAML. Otherwise, this PR is good to merge.
Review by security
Evaluated against
26f326cf@@ -87,0 +157,4 @@// unmarshalYAMLWithDepthLimit unmarshals YAML data with explicit depth limiting// and strict field checking. This protects against stack exhaustion from deeply// nested structures and catches typos in field names.// Multi-document YAML files are rejected to prevent silent data loss.[MINOR] JSON parsing uses json.Unmarshal, which silently ignores unknown fields. For consistency with YAML's KnownFields(true) and to harden against typos or unintended keys, consider switching to json.Decoder with DisallowUnknownFields() to reject unknown JSON fields.
Gpt Review
Summary
Well-implemented YAML support with sensible size/depth safeguards and comprehensive tests. Uses the approved yaml.v3 dependency, maintains backward compatibility, and updates docs appropriately.
Findings
review/persona.goreview/persona.godocs/DESIGN-57-yaml-persona.mdRecommendation
APPROVE — The implementation is solid: format detection is case-insensitive, YAML parsing includes depth and node-count limits, multi-document files are rejected, and strict field checking prevents typos. Tests cover valid/invalid YAML, JSON compatibility, limits, and built-in personas. The dependency adheres to the repository allowlist. Approve as-is. Consider future refinements: allow symlinked persona files if needed and enforce DisallowUnknownFields for JSON to align with YAML strictness. Also update the design doc for consistency with the final embedding approach.
Review by gpt
Evaluated against
26f326cf@@ -0,0 +32,4 @@```### YAML Parsing with Depth Protection[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.
@@ -37,0 +56,4 @@if err != nil {return nil, fmt.Errorf("read persona file %s: %w", path, err)}if !info.Mode().IsRegular() {[MINOR] LoadPersona rejects non-regular files (e.g., symlinks) by checking Mode().IsRegular(). This may be overly strict for CI or repo setups that use symlinks. Consider allowing symlinks by resolving them (os.Stat vs. Lstat) or relaxing the check.
@@ -79,0 +143,4 @@if isYAML {err = unmarshalYAMLWithDepthLimit(data, &p, MaxYAMLDepth)} else {err = json.Unmarshal(data, &p)[MINOR] JSON parsing uses json.Unmarshal which accepts unknown fields, while YAML is parsed with KnownFields(true). For parity and to catch typos in JSON persona files, consider switching to a json.Decoder with DisallowUnknownFields().
Self-review: NEEDS_WORK
issue-57 — 2026-05-10
[fit] Dead code in
LoadBuiltinPersona: The JSON fallback (data, err = embeddedPersonas.ReadFile("personas/" + jsonFile)) can never succeed because://go:embed personas/*.yaml(no JSON files embedded)This creates the illusion of backwards compatibility that doesn't exist. Either:
//go:embed personas/*.jsonif JSON built-ins should be supported[completeness] Design doc checklist item 1 says "go-yaml dependency added at v1.16.0+" but the library is
gopkg.in/yaml.v3(v3.0.1). The version reference is for a different library (github.com/goccy/go-yaml). Minor doc nit - doesn't block but is misleading.What's Good
Verdict
The dead code issue should be addressed before merge. It's a minor fix (delete ~6 lines) but leaving dead code that suggests non-existent functionality is a code smell.
Addressed Review Findings (commit
10cd620)Fixed all open review findings:
1. Dead JSON fallback code (self-review NEEDS_WORK)
Removed the dead code path in
LoadBuiltinPersonathat attempted JSON fallback. Since the embed directive only includes*.yamlfiles, this code could never succeed.2. JSON parsing inconsistency (security-review-bot MINOR + gpt-review-bot MINOR #2)
Switched from
json.Unmarshaltojson.DecoderwithDisallowUnknownFields(). This matches YAML'sKnownFields(true)behavior — both formats now reject unknown fields to catch typos.Added test
TestJSONUnknownFieldsRejectedwith coverage for:3. Symlink handling documentation (gpt-review-bot MINOR #1)
Verified that
os.Stat()already follows symlinks, soMode().IsRegular()operates on the target file, not the symlink itself. Symlinks pointing to regular files work correctly.Added:
os.StatcallTestLoadPersonaSymlinkverifying symlink supportAll tests pass (
go test ./...andgo vet ./...).Self-review: PASS
No issues found — ready for human review.
Changes reviewed:
Pre-commit gates:
go test ./...✅go vet ./...✅scripts/check-deps.sh✅Sonnet Review
Summary
Well-implemented YAML support for persona files. The code uses the approved
gopkg.in/yaml.v3dependency, has robust DoS protections (depth limit, node count, file size, multi-document rejection), comprehensive tests, and good documentation. CI passes.Findings
review/persona.godec.Decode(&extra) == nilconsumes IO from the first-pass decoder, but the strict second-pass re-decodes frombytes.NewReader(data), so correctness is fine. However, the document count check is somewhat brittle: it silently ignores any decode error on the second document (including genuine IO errors) by treating any non-nil error as 'only one document'. In practice this works since the reader is in-memory, but the intent would be clearer withif err := dec.Decode(&extra); !errors.Is(err, io.EOF) && err == nil { return fmt.Errorf(...) }or by explicitly checkingerr == nil.review/persona.goListBuiltinPersonasdeduplication map (seen[personaName] = true) is correct but slightly redundant: the conditionif !seen[personaName]is always true on the first encounter (the only time the body runs), so the inner check adds noise. A simpler pattern would beif !seen[personaName] { seen[personaName] = true; names = append(names, name) }, building the slice directly instead of a two-pass map → slice conversion. Not a bug, just unnecessary complexity.docs/DESIGN-57-yaml-persona.mdreview/persona.goreview/persona_test.goTestYAMLAliasCycleDetectionandTestCheckYAMLDepthCycleDetectionDirecttest nearly identical scenarios (artificial AliasNode cycle). They could be merged into one test with subtly different configurations to reduce duplication.Recommendation
APPROVE — Approve. CI passes and the implementation is solid. The YAML parsing is well-protected against DoS vectors (depth, width, file size, multi-document) and uses the correct approved dependency. The two MINOR findings are not bugs — one is a slightly fragile multi-document detection pattern (works fine in practice) and one is unnecessary map complexity in
ListBuiltinPersonas. The nits are cosmetic. The feature is complete, well-tested, and backwards-compatible.Review by sonnet
Evaluated against
10cd6203@@ -0,0 +52,4 @@}// Handle alias nodes by following the Alias pointerif node.Kind == yaml.AliasNode && node.Alias != nil {return checkYAMLDepth(node.Alias, depth, maxDepth)[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
checkYAMLDepthwalker, not the library itself. The description is misleading and may cause confusion during future maintenance.@@ -76,1 +134,4 @@// Format is detected by the source file extension.func parsePersona(data []byte, source string) (*Persona, error) {lowerSource := strings.ToLower(source)isYAML := strings.HasSuffix(lowerSource, ".yaml") || strings.HasSuffix(lowerSource, ".yml")[MINOR] The
ListBuiltinPersonasdeduplication map (seen[personaName] = true) is correct but slightly redundant: the conditionif !seen[personaName]is always true on the first encounter (the only time the body runs), so the inner check adds noise. A simpler pattern would beif !seen[personaName] { seen[personaName] = true; names = append(names, name) }, building the slice directly instead of a two-pass map → slice conversion. Not a bug, just unnecessary complexity.@@ -87,0 +185,4 @@// KnownFields(true) rejects unknown keys, catching typos like "focuss" or "identiy".// We must re-decode from the original data because yaml.Node.Decode() doesn't// support the KnownFields option.strictDec := yaml.NewDecoder(bytes.NewReader(data))[MINOR] The multi-document check
dec.Decode(&extra) == nilconsumes IO from the first-pass decoder, but the strict second-pass re-decodes frombytes.NewReader(data), so correctness is fine. However, the document count check is somewhat brittle: it silently ignores any decode error on the second document (including genuine IO errors) by treating any non-nil error as 'only one document'. In practice this works since the reader is in-memory, but the intent would be clearer withif err := dec.Decode(&extra); !errors.Is(err, io.EOF) && err == nil { return fmt.Errorf(...) }or by explicitly checkingerr == nil.@@ -87,0 +216,4 @@return checkYAMLDepth(node.Alias, depth+1, maxDepth, maxNodes, seen, nodeCount)}for _, child := range node.Content {[NIT] The cycle detection comment says 'Already validated this subtree, skip to avoid infinite recursion' but returning nil on a revisited node means that node's depth contribution is not re-checked at the new depth. This is intentional (and correct for cycle-breaking), but the comment could be clearer that depth checking for shared/aliased subtrees is only done once from the first encounter.
@@ -240,0 +446,4 @@}// Comments should be ignoredif p.Name != "commented" {[NIT] Both
TestYAMLAliasCycleDetectionandTestCheckYAMLDepthCycleDetectionDirecttest nearly identical scenarios (artificial AliasNode cycle). They could be merged into one test with subtly different configurations to reduce duplication.Security Review
Summary
The changes add YAML support with thoughtful, defense-in-depth safeguards: strict file size limits, single-document enforcement, depth and node-count checks, and strict field validation for both YAML and JSON. CI passed and no exploitable security issues were identified.
Recommendation
APPROVE — Approve the PR. The YAML parsing path is implemented with strong security hygiene (size limits, depth/node checks, KnownFields, and TOCTOU size re-check). Built-in personas are embedded safely, and extension-based format detection is case-insensitive. If desired for additional defense-in-depth, consider future refinements like opening files then validating type to further reduce TOCTOU windows, but this is not required given the current threat model and constraints.
Review by security
Evaluated against
10cd6203Gpt Review
Summary
Solid addition of YAML support with thoughtful security hardening (depth and node count limits), strict unknown-field checks for both YAML and JSON, and comprehensive tests. Documentation and embedded personas are updated consistently with repository conventions and minimal dependencies.
Findings
review/persona.goreview/persona.goRecommendation
APPROVE — The implementation is robust, idiomatic, and aligns with repo conventions (only stdlib + gopkg.in/yaml.v3). YAML parsing includes strict field validation and DoS protections (depth and node limits). Tests are comprehensive and cover both YAML and JSON paths, edge cases, and security constraints. As a minor improvement, update JSON parsing to enforce single-document input by checking for EOF after the initial Decode, mirroring the strictness applied to YAML. The extra handling for other extensions in ListBuiltinPersonas is fine but could be simplified. With these small enhancements optional, this change is ready to merge.
Review by gpt
Evaluated against
10cd6203@@ -71,0 +109,4 @@// Strip extension to get persona namevar personaName stringswitch {case strings.HasSuffix(name, ".yaml"):[NIT] ListBuiltinPersonas handles .yml and .json extensions even though only *.yaml files are embedded. This is harmless but could be simplified or clarified in comments to reflect the embed pattern.
@@ -79,0 +145,4 @@// YAML's KnownFields(true) - both reject unknown fields to catch typos.dec := json.NewDecoder(bytes.NewReader(data))dec.DisallowUnknownFields()err = dec.Decode(&p)[MINOR] JSON parsing does not enforce single-document input. After dec.Decode(&p), any trailing JSON values would be silently ignored. Consider verifying EOF by attempting a second decode and expecting io.EOF to ensure there's no extra data.