-
Notifications
You must be signed in to change notification settings - Fork 73
fix(#5448): use field-level merge for validation_loop in base composition #5450
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
797a658
05b2a68
36dcd1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -421,12 +421,43 @@ func matchingAllowedPrefix(rawURL string, allowlist []string) string { | |
| return MatchingAllowedPrefixInList(rawURL, allowlist) | ||
| } | ||
|
|
||
| // mergeValidationLoop merges two ValidationLoop pointers field by field. | ||
| // Child non-zero values win; base fills gaps. Returns the merged result. | ||
| // If both are nil, returns nil. If only one is non-nil, returns a copy of it. | ||
| func mergeValidationLoop(base, child *ValidationLoop) *ValidationLoop { | ||
| if base == nil && child == nil { | ||
| return nil | ||
| } | ||
| if base == nil { | ||
| return child | ||
| } | ||
| if child == nil { | ||
| return base | ||
| } | ||
|
|
||
| merged := *child // start with child values | ||
|
waynesun09 marked this conversation as resolved.
|
||
| if merged.Script == "" { | ||
| merged.Script = base.Script | ||
| } | ||
| if merged.Schema == "" { | ||
| merged.Schema = base.Schema | ||
| } | ||
| if merged.MaxIterations == 0 { | ||
| merged.MaxIterations = base.MaxIterations | ||
| } | ||
| if merged.FeedbackMode == "" { | ||
| merged.FeedbackMode = base.FeedbackMode | ||
| } | ||
| return &merged | ||
| } | ||
|
|
||
| // mergeBaseIntoChild merges base harness fields into child harness. | ||
| // Child values override base values following ADR-0045 merge rules: | ||
| // - Scalars: child overrides if non-zero | ||
| // - Slices (skills, plugins, providers, api_servers): base + child (concatenated) | ||
| // - Maps (runner_env): base merged with child; child keys win | ||
| // - Pointer structs (validation_loop, security): child replaces if non-nil | ||
| // - Pointer structs (validation_loop): field-level merge; child non-zero fields win | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MEDIUM] This comment (and its counterpart in forge.go's Suggestion: Amend ADR-0045's inheritance table, base-merge-rules list, and open-questions note for (Flagged independently by 2 of 3 review agents; both quoted the exact stale ADR passages.) |
||
| // - Pointer structs (security): child replaces if non-nil | ||
| // - host_files: concatenated with last-writer-wins dedup by Dest | ||
| // - forge: key-by-key merge; per-platform uses same rules | ||
| // - allowed_remote_resources: NOT merged (security; child must declare its own) | ||
|
|
@@ -536,10 +567,8 @@ func mergeBaseIntoChild(base, child *Harness) { | |
| child.Env.mergeEnvFrom(base.Env, false) | ||
| } | ||
|
|
||
| // Pointer structs: child replaces if non-nil | ||
| if child.ValidationLoop == nil { | ||
| child.ValidationLoop = base.ValidationLoop | ||
| } | ||
| // ValidationLoop: field-level merge; child non-zero fields win | ||
| child.ValidationLoop = mergeValidationLoop(base.ValidationLoop, child.ValidationLoop) | ||
| // Security: child inherits base's config if nil. Note that a base harness | ||
| // (even integrity-pinned) could set fail_mode: open. Child authors must | ||
| // explicitly set their own security block to prevent inheriting a weaker posture. | ||
|
|
@@ -1272,10 +1301,8 @@ func mergeForgeConfigInto(base, child *ForgeConfig) { | |
| child.Env.mergeEnvFrom(base.Env, false) | ||
| } | ||
|
|
||
| // ValidationLoop: child replaces if non-nil | ||
| if child.ValidationLoop == nil { | ||
| child.ValidationLoop = base.ValidationLoop | ||
| } | ||
| // ValidationLoop: field-level merge; child non-zero fields win | ||
| child.ValidationLoop = mergeValidationLoop(base.ValidationLoop, child.ValidationLoop) | ||
| } | ||
|
|
||
| // FetchAgentHarness fetches a URL-sourced agent harness using the same | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[HIGH] —
DiffHarnesswas not updated to match the new field-level merge, breaking the base/child round-trip used bymigrate-customizationsADR-0045's Consequences section states the
base:merge rules and their inverse (DiffHarness) must change together ("Changes to merge rules must be reflected in both directions"), but this PR only updates the merge side.DiffHarness(internal/harness/diff.go:179-186) anddiffForgeConfig(diff.go:470-473) still treatValidationLoopas whole-struct:if child.ValidationLoop != nil && !reflect.DeepEqual(child.ValidationLoop, base.ValidationLoop) { result.Child.ValidationLoop = child.ValidationLoop }. When old whole-struct-replace merge was in effect, copying the entire child struct into the diff output was correct. With the new field-levelmergeValidationLoop, it's not: if the child'sValidationLoopdiffers from base in just one field (sayScript) while another field (saySchema) is genuinely empty/unset for that child, the whole child struct — including that genuine emptySchema— gets copied into the "minimal" diff. Re-composing that diffed-out child against the same base viamergeBaseIntoChildthen backfillsSchemafrom base, silently changing effective validation config.This is reachable in production:
internal/cli/migrate.go:479callsharness.DiffHarness(&upstreamHarness, &customizedHarness, ...)as the core of themigrate-customizationscommand (ADR-0064), which converts standalone customized harnesses intobase:-composed ones — exactly the diff → later-remerge path described above.Suggestion: Diff
ValidationLoopfield-by-field inDiffHarness/diffForgeConfig(mirroringmergeValidationLoop's field set), consistent with howRunnerEnv/Envare already diffed field-by-field elsewhere in the same file. Add a round-trip test where base setsschema/feedback_modeand the differing child intentionally leaves them zero.(Independently identified by 2 of 3 review agents, both verified by tracing the actual code paths.)