refactor: clone ParsedPolicy in processPolicy to prevent shared-state races - #7794
Conversation
|
This pull request does not have a backport label. Could you fix it @ycombinator? 🙏
|
b4dd4ec to
9541e07
Compare
There was a problem hiding this comment.
🟡 Changes recommended
ParsedPolicy.Clone() currently copies pp.Policy without cloning Policy.Namespaces, so the implementation does not fully match its stated “independent backing storage” guarantee and may still allow shared-slice races if that field is ever mutated.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR refactors policy handling to eliminate shared-state mutation in processPolicy by introducing a ParsedPolicy.Clone() method and switching processPolicy to operate on a per-call cloned ParsedPolicy, preventing cross-agent goroutines from racing when preparing policy actions.
Changes:
- Added
(*policy.ParsedPolicy).Clone()to copyParsedPolicyslice/map fields into independent backing storage. - Updated
processPolicyto clone theParsedPolicyup front and mutatepp.SecretKeyson the clone (removing the narrowersecretKeys := slices.Clone(...)approach).
File summaries
| File | Description |
|---|---|
internal/pkg/policy/parsed_policy.go |
Adds ParsedPolicy.Clone() to make per-goroutine copies of mutable slice/map fields. |
internal/pkg/api/handleCheckin.go |
Updates processPolicy to use pp = pp.Clone() and mutate secret key paths on the cloned policy instance. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
9541e07 to
8ef7eee
Compare
There was a problem hiding this comment.
🟡 Changes recommended
ParsedPolicy.Clone() currently still shares some backing storage/pointers despite claiming full isolation, which weakens the race-safety contract this PR is relying on.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
ParsedPolicy.Clone() still shallow-copies mutable nested maps (notably OTel exporter config maps), so processPolicy can still race when it mutates those configs.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
internal/pkg/policy/parsed_policy.go:76
Clone()currently relies onmodel.ClonePolicyData, which shallow-clones OTel maps likePolicy.Data.Exporters(only the outer map).prepareOTelExportersmutates each exporter config map in-place, so if exporter entries aremap[string]any(as expected), those inner maps can still be shared across goroutines and race. Consider deep-cloning the exporter config maps (and other mutablePolicy.Datamaps if needed) insideClone()to make the copy actually mutation-safe.
clone.Policy.Data = model.ClonePolicyData(pp.Policy.Data)
clone.Policy.Namespaces = slices.Clone(pp.Policy.Namespaces)
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
8ef7eee to
09154ab
Compare
09154ab to
fac89aa
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🟡 Changes recommended
The new cloning still leaves some mutable policy data potentially shared (and lacks focused unit coverage for clone-independence), which can undermine the intended race-prevention guarantee.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
internal/pkg/policy/parsed_policy.go:64
- ParsedPolicy.Clone is new concurrency-safety plumbing for processPolicy, but there are no unit tests validating that the clone is actually mutation-independent (e.g., SecretKeys slice and nested policy data such as OTel exporter configs). Adding a focused test would help prevent regressions in this area.
// Clone returns a copy of pp where every slice, map, and pointer field has its
// own backing storage, so concurrent goroutines can mutate their copy without racing.
func (pp *ParsedPolicy) Clone() *ParsedPolicy {
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
This comment has been minimized.
This comment has been minimized.
fac89aa to
d039361
Compare
maps.Clone on Agent and Fleet, and per-element maps.Clone on Inputs, were shallow clones that implied deeper isolation than they provided. processPolicy never accesses Agent or Fleet, and never mutates Inputs elements — only overwrites the slice header via Policy.Data.Inputs. Replace with direct assignment (Agent, Fleet) and slices.Clone (Inputs) and document the invariant. Remove the now-incorrect Agent isolation assertion from TestParsedPolicyCloneIsolation. Also drops the unused "maps" import. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
🟡 Changes recommended
ParsedPolicy.Clone() currently leaves Agent/Fleet pointing at the original maps (while Policy.Data.Agent/Fleet are cloned), breaking aliasing invariants and retaining shared mutable state that undermines the intended ownership-transfer semantics.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
NewParsedPolicy assigns pp.Agent = p.Data.Agent and pp.Fleet = p.Data.Fleet, so the two fields alias the same map. Clone() was leaving clone.Agent/Fleet pointing at the original's maps while clone.Policy.Data had freshly-cloned maps from ClonePolicyData, breaking the invariant. Set clone.Agent and clone.Fleet from the cloned Policy.Data after ClonePolicyData runs, preserving the alias on the clone side. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
🟡 Changes recommended
The new clone-isolation test currently doesn’t reliably exercise nested-map sharing for outputs and includes a misleading comment, weakening regression protection for the concurrency fix.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
internal/pkg/policy/parsed_policy_test.go:131
- The nested-map isolation assertion can be a no-op with the current embedded test_policy.json because its outputs don't include an existing nested map (e.g., no "ssl" object). In that case, the test only proves top-level output-map isolation and would still pass if ClonePolicyData shallow-cloned nested maps/slices. Seed a nested map in the original before cloning so the test reliably detects nested sharing regressions.
// Seed a known secret key so we can detect cross-contamination.
original.SecretKeys = []string{"outputs.default.token"}
clone := original.Clone()
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
After 0f62096 clone.Agent is derived from the cloned Policy.Data (not a shared reference), so the stale comment is removed and the top-level isolation assertion is restored. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
🟢 Approval recommended
The changes correctly establish per-subscriber ownership of mutable policy state, and include targeted tests to prevent regressions of the reported race.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
@Mergifyio backport 9.5 9.4 8.19 |
✅ Backports have been createdDetails
Cherry-pick of c907276 has failed: To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally
Cherry-pick of c907276 has failed: To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally
Cherry-pick of c907276 has failed: To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally |
Conflict markers from the Mergify auto-backport of #7794 are resolved: - handleCheckin.go: use pp.Policy.Data.Outputs (9.5 Prepare has no options param) - handleCheckin_test.go: call processPolicy with pp.Clone() (no nil collector arg) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…iles Conflict markers from the Mergify auto-backport of #7794 are resolved: - ext.go: use nil Inputs (populated below) and bytes.Clone(OutputPermissions) - handleCheckin.go: use pp.Policy.Data.Outputs (9.4 Prepare has no options param) - handleCheckin_test.go: call processPolicy with pp.Clone() (no nil collector arg) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Conflict markers from the Mergify auto-backport of #7794 resolved: - ext.go: use bytes.Clone for json.RawMessage Agent/Fleet (8.19 types), take nil Inputs and bytes.Clone(OutputPermissions), add deepCloneMapAny/ deepCloneSliceAny helpers; skip OTel fields absent from 8.19 PolicyData - parsed_policy.go: remove Agent/Fleet field assignments absent from 8.19 ParsedPolicy struct - handleCheckin.go: use pp.Policy.Data.Outputs directly (pp is a clone); update SecretKeys on pp; skip prepareOTelExporters (not in 8.19); keep &keys pointer for *[]string SecretPaths type in 8.19 API - handleCheckin_test.go: call processPolicy with pp.Clone() (keep HEAD goroutine structure; no nil collector arg) - monitor.go: take cherry-pick push-back on rate-limit error and ctx cancel - parsed_policy_test.go: add TestParsedPolicyCloneIsolation without Agent field references (absent from 8.19 ParsedPolicy); skip OTel/secrets tests Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…to prevent shared-state races (#7803) * refactor: clone ParsedPolicy in processPolicy to prevent shared-state races (#7794) * refactor: clone ParsedPolicy at dispatch site to prevent shared-state races Move the ParsedPolicy clone from processPolicy to the monitor dispatch loop, establishing ownership at the point where the policy transitions from the shared monitor cache to an individual subscriber. Each channel send now transfers an exclusively-owned *ParsedPolicy to its subscriber, so processPolicy can treat its argument as its own. Changes: - monitor.go: compute cloned := policy.pp.Clone() before the select so it is not embedded in the send expression (Go evaluates all select case expressions on entry; doing clone inside the send ran it even when ctx.Done/default fired under the held mutex) - handleCheckin.go: processPolicy no longer needs to clone pp - handleCheckin_test.go: pass pp.Clone() per goroutine in the concurrent regression test, mirroring what the monitor now does at dispatch time - ParsedPolicy.Clone() deep-copies every slice, map, and pointer field: SecretKeys and Policy.Namespaces via slices.Clone; Inputs, Roles (including RoleT.Raw bytes), and Outputs (including Output.Role pointers) element by element; Agent and Fleet via maps.Clone; Policy.Data via model.ClonePolicyData - model.ClonePolicyData now also clones Agent/Fleet maps, OutputPermissions bytes, and output/OTel section maps via deepCloneMapAny — a new recursive helper that deep-clones map[string]any and []any trees; necessary because ProcessOutputSecret and prepareOTelExporters mutate nested map entries in-place; nil Inputs is preserved as nil - TestParsedPolicyCloneIsolation verifies that mutating each field of a clone does not affect the original; SecretKeys check mutates an existing element (not append) to detect shared backing storage; Outputs check mutates both top-level and nested (ssl.key) paths to catch shallow-clone regressions Relates #7739 Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: skip Clone() when ctx is already cancelled in dispatchPending Guard policy.pp.Clone() with a ctx.Err() check so the potentially expensive clone does not run while holding m.mut when the context was already cancelled between m.limit.Wait and the clone site. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * refactor: narrow m.mut critical section in dispatchPending Hold m.mut only for queue/map operations (popFront, pushFront, policies lookup). Release it before m.limit.Wait, Clone(), and channel sends so Subscribe/Unsubscribe/updatePolicy are not blocked during those potentially slow operations. policyT is a value type so the map lookup copies the struct, making it safe to use policy.pp after the lock is released. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * refactor: use direct assignment for Agent/Fleet/Inputs in Clone() maps.Clone on Agent and Fleet, and per-element maps.Clone on Inputs, were shallow clones that implied deeper isolation than they provided. processPolicy never accesses Agent or Fleet, and never mutates Inputs elements — only overwrites the slice header via Policy.Data.Inputs. Replace with direct assignment (Agent, Fleet) and slices.Clone (Inputs) and document the invariant. Remove the now-incorrect Agent isolation assertion from TestParsedPolicyCloneIsolation. Also drops the unused "maps" import. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: restore Agent/Fleet aliasing invariant in ParsedPolicy.Clone() NewParsedPolicy assigns pp.Agent = p.Data.Agent and pp.Fleet = p.Data.Fleet, so the two fields alias the same map. Clone() was leaving clone.Agent/Fleet pointing at the original's maps while clone.Policy.Data had freshly-cloned maps from ClonePolicyData, breaking the invariant. Set clone.Agent and clone.Fleet from the cloned Policy.Data after ClonePolicyData runs, preserving the alias on the clone side. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test: restore Agent isolation assertion after Clone() aliasing fix After 0f62096 clone.Agent is derived from the cloned Policy.Data (not a shared reference), so the stale comment is removed and the top-level isolation assertion is restored. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> (cherry picked from commit c907276) # Conflicts: # internal/pkg/api/handleCheckin.go # internal/pkg/api/handleCheckin_test.go # internal/pkg/model/ext.go # internal/pkg/policy/monitor.go # internal/pkg/policy/parsed_policy_test.go * fix: resolve backport conflict markers in 8.19 branch Conflict markers from the Mergify auto-backport of #7794 resolved: - ext.go: use bytes.Clone for json.RawMessage Agent/Fleet (8.19 types), take nil Inputs and bytes.Clone(OutputPermissions), add deepCloneMapAny/ deepCloneSliceAny helpers; skip OTel fields absent from 8.19 PolicyData - parsed_policy.go: remove Agent/Fleet field assignments absent from 8.19 ParsedPolicy struct - handleCheckin.go: use pp.Policy.Data.Outputs directly (pp is a clone); update SecretKeys on pp; skip prepareOTelExporters (not in 8.19); keep &keys pointer for *[]string SecretPaths type in 8.19 API - handleCheckin_test.go: call processPolicy with pp.Clone() (keep HEAD goroutine structure; no nil collector arg) - monitor.go: take cherry-pick push-back on rate-limit error and ctx cancel - parsed_policy_test.go: add TestParsedPolicyCloneIsolation without Agent field references (absent from 8.19 ParsedPolicy); skip OTel/secrets tests Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: remove unused cloneOTelSection and extra blank line in 8.19 ext.go cloneOTelSection was introduced by the cherry-pick but is unused in 8.19 since PolicyData has no OTel fields. The extra blank line left by the conflict-resolution commit caused a goimports formatting failure. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Shaunak Kashyap <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]>
…o prevent shared-state races (#7801) * refactor: clone ParsedPolicy in processPolicy to prevent shared-state races (#7794) * refactor: clone ParsedPolicy at dispatch site to prevent shared-state races Move the ParsedPolicy clone from processPolicy to the monitor dispatch loop, establishing ownership at the point where the policy transitions from the shared monitor cache to an individual subscriber. Each channel send now transfers an exclusively-owned *ParsedPolicy to its subscriber, so processPolicy can treat its argument as its own. Changes: - monitor.go: compute cloned := policy.pp.Clone() before the select so it is not embedded in the send expression (Go evaluates all select case expressions on entry; doing clone inside the send ran it even when ctx.Done/default fired under the held mutex) - handleCheckin.go: processPolicy no longer needs to clone pp - handleCheckin_test.go: pass pp.Clone() per goroutine in the concurrent regression test, mirroring what the monitor now does at dispatch time - ParsedPolicy.Clone() deep-copies every slice, map, and pointer field: SecretKeys and Policy.Namespaces via slices.Clone; Inputs, Roles (including RoleT.Raw bytes), and Outputs (including Output.Role pointers) element by element; Agent and Fleet via maps.Clone; Policy.Data via model.ClonePolicyData - model.ClonePolicyData now also clones Agent/Fleet maps, OutputPermissions bytes, and output/OTel section maps via deepCloneMapAny — a new recursive helper that deep-clones map[string]any and []any trees; necessary because ProcessOutputSecret and prepareOTelExporters mutate nested map entries in-place; nil Inputs is preserved as nil - TestParsedPolicyCloneIsolation verifies that mutating each field of a clone does not affect the original; SecretKeys check mutates an existing element (not append) to detect shared backing storage; Outputs check mutates both top-level and nested (ssl.key) paths to catch shallow-clone regressions Relates #7739 Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: skip Clone() when ctx is already cancelled in dispatchPending Guard policy.pp.Clone() with a ctx.Err() check so the potentially expensive clone does not run while holding m.mut when the context was already cancelled between m.limit.Wait and the clone site. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * refactor: narrow m.mut critical section in dispatchPending Hold m.mut only for queue/map operations (popFront, pushFront, policies lookup). Release it before m.limit.Wait, Clone(), and channel sends so Subscribe/Unsubscribe/updatePolicy are not blocked during those potentially slow operations. policyT is a value type so the map lookup copies the struct, making it safe to use policy.pp after the lock is released. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * refactor: use direct assignment for Agent/Fleet/Inputs in Clone() maps.Clone on Agent and Fleet, and per-element maps.Clone on Inputs, were shallow clones that implied deeper isolation than they provided. processPolicy never accesses Agent or Fleet, and never mutates Inputs elements — only overwrites the slice header via Policy.Data.Inputs. Replace with direct assignment (Agent, Fleet) and slices.Clone (Inputs) and document the invariant. Remove the now-incorrect Agent isolation assertion from TestParsedPolicyCloneIsolation. Also drops the unused "maps" import. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: restore Agent/Fleet aliasing invariant in ParsedPolicy.Clone() NewParsedPolicy assigns pp.Agent = p.Data.Agent and pp.Fleet = p.Data.Fleet, so the two fields alias the same map. Clone() was leaving clone.Agent/Fleet pointing at the original's maps while clone.Policy.Data had freshly-cloned maps from ClonePolicyData, breaking the invariant. Set clone.Agent and clone.Fleet from the cloned Policy.Data after ClonePolicyData runs, preserving the alias on the clone side. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test: restore Agent isolation assertion after Clone() aliasing fix After 0f62096 clone.Agent is derived from the cloned Policy.Data (not a shared reference), so the stale comment is removed and the top-level isolation assertion is restored. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> (cherry picked from commit c907276) # Conflicts: # internal/pkg/api/handleCheckin.go # internal/pkg/api/handleCheckin_test.go * fix: resolve backport conflict markers in 9.5 handleCheckin files Conflict markers from the Mergify auto-backport of #7794 are resolved: - handleCheckin.go: use pp.Policy.Data.Outputs (9.5 Prepare has no options param) - handleCheckin_test.go: call processPolicy with pp.Clone() (no nil collector arg) Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Shaunak Kashyap <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]>
…o prevent shared-state races (#7802) * refactor: clone ParsedPolicy in processPolicy to prevent shared-state races (#7794) * refactor: clone ParsedPolicy at dispatch site to prevent shared-state races Move the ParsedPolicy clone from processPolicy to the monitor dispatch loop, establishing ownership at the point where the policy transitions from the shared monitor cache to an individual subscriber. Each channel send now transfers an exclusively-owned *ParsedPolicy to its subscriber, so processPolicy can treat its argument as its own. Changes: - monitor.go: compute cloned := policy.pp.Clone() before the select so it is not embedded in the send expression (Go evaluates all select case expressions on entry; doing clone inside the send ran it even when ctx.Done/default fired under the held mutex) - handleCheckin.go: processPolicy no longer needs to clone pp - handleCheckin_test.go: pass pp.Clone() per goroutine in the concurrent regression test, mirroring what the monitor now does at dispatch time - ParsedPolicy.Clone() deep-copies every slice, map, and pointer field: SecretKeys and Policy.Namespaces via slices.Clone; Inputs, Roles (including RoleT.Raw bytes), and Outputs (including Output.Role pointers) element by element; Agent and Fleet via maps.Clone; Policy.Data via model.ClonePolicyData - model.ClonePolicyData now also clones Agent/Fleet maps, OutputPermissions bytes, and output/OTel section maps via deepCloneMapAny — a new recursive helper that deep-clones map[string]any and []any trees; necessary because ProcessOutputSecret and prepareOTelExporters mutate nested map entries in-place; nil Inputs is preserved as nil - TestParsedPolicyCloneIsolation verifies that mutating each field of a clone does not affect the original; SecretKeys check mutates an existing element (not append) to detect shared backing storage; Outputs check mutates both top-level and nested (ssl.key) paths to catch shallow-clone regressions Relates #7739 Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: skip Clone() when ctx is already cancelled in dispatchPending Guard policy.pp.Clone() with a ctx.Err() check so the potentially expensive clone does not run while holding m.mut when the context was already cancelled between m.limit.Wait and the clone site. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * refactor: narrow m.mut critical section in dispatchPending Hold m.mut only for queue/map operations (popFront, pushFront, policies lookup). Release it before m.limit.Wait, Clone(), and channel sends so Subscribe/Unsubscribe/updatePolicy are not blocked during those potentially slow operations. policyT is a value type so the map lookup copies the struct, making it safe to use policy.pp after the lock is released. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * refactor: use direct assignment for Agent/Fleet/Inputs in Clone() maps.Clone on Agent and Fleet, and per-element maps.Clone on Inputs, were shallow clones that implied deeper isolation than they provided. processPolicy never accesses Agent or Fleet, and never mutates Inputs elements — only overwrites the slice header via Policy.Data.Inputs. Replace with direct assignment (Agent, Fleet) and slices.Clone (Inputs) and document the invariant. Remove the now-incorrect Agent isolation assertion from TestParsedPolicyCloneIsolation. Also drops the unused "maps" import. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: restore Agent/Fleet aliasing invariant in ParsedPolicy.Clone() NewParsedPolicy assigns pp.Agent = p.Data.Agent and pp.Fleet = p.Data.Fleet, so the two fields alias the same map. Clone() was leaving clone.Agent/Fleet pointing at the original's maps while clone.Policy.Data had freshly-cloned maps from ClonePolicyData, breaking the invariant. Set clone.Agent and clone.Fleet from the cloned Policy.Data after ClonePolicyData runs, preserving the alias on the clone side. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test: restore Agent isolation assertion after Clone() aliasing fix After 0f62096 clone.Agent is derived from the cloned Policy.Data (not a shared reference), so the stale comment is removed and the top-level isolation assertion is restored. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> (cherry picked from commit c907276) # Conflicts: # internal/pkg/api/handleCheckin.go # internal/pkg/api/handleCheckin_test.go # internal/pkg/model/ext.go * fix: resolve backport conflict markers in 9.4 handleCheckin and ext files Conflict markers from the Mergify auto-backport of #7794 are resolved: - ext.go: use nil Inputs (populated below) and bytes.Clone(OutputPermissions) - handleCheckin.go: use pp.Policy.Data.Outputs (9.4 Prepare has no options param) - handleCheckin_test.go: call processPolicy with pp.Clone() (no nil collector arg) Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Shaunak Kashyap <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Summary
ParsedPolicy.Clone()which returns a fully independent copy — every slice, map, and pointer field gets its own backing storage:SecretKeysandPolicy.Namespacesviaslices.Clone;Inputs,Roles(includingRoleT.Rawbytes), andOutputs(includingOutput.Rolepointers) deep-copied element by element;AgentandFleetviamaps.Clone;Policy.Dataviamodel.ClonePolicyData.model.ClonePolicyDatato clone itsAgentandFleetmaps,OutputPermissionsbytes, and OTel section maps viacloneOTelSection— necessary becauseprepareOTelExportersmutates per-component maps in-place;nilInputsis preserved asnil.monitor.go:s.ch <- policy.pp.Clone()instead of&policy.pp. Each channel send now transfers an exclusively-owned*ParsedPolicyto its subscriber, soprocessPolicyreceives a copy it already owns and can mutate freely — no implicit contract that callers must clone.processPolicyis unchanged in behaviour; the clone just moves upstream to the natural ownership-transfer boundary.TestParsedPolicyCloneIsolationto verify that mutating each field of a clone does not affect the original.References
Relates #7739
🤖 Generated with Claude Code