Skip to content

fix(engine): stop parseAmsPolicySpec aliasing the frozen DEFAULT_AMS_POLICY_SPEC sub-objects - #10069

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
RealDiligent:fix/critical-issue-ams-policy-spec-clone-9995
Jul 31, 2026
Merged

fix(engine): stop parseAmsPolicySpec aliasing the frozen DEFAULT_AMS_POLICY_SPEC sub-objects#10069
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
RealDiligent:fix/critical-issue-ams-policy-spec-clone-9995

Conversation

@RealDiligent

Copy link
Copy Markdown
Contributor

Summary

DEFAULT_AMS_POLICY_SPEC (packages/loopover-engine/src/ams-policy-spec.ts) is a deep-frozen shared singleton, and its own doc says "clone before layering overrides on top." But normalizeCapLimits and normalizeConvergenceThresholds returned their fallback — which parseAmsPolicySpec passes as DEFAULT_AMS_POLICY_SPEC.capLimits / .convergenceThresholdsby reference on the undefined/null and not-a-mapping paths (only the valid-mapping path built a fresh object).

So a parsed spec that carries at least one configured field (and therefore returns the normalized spec at line 389, not the all-defaults cloneDefaultAmsPolicySpec() at line 387) aliased the frozen singleton's sub-objects into a caller's mutable spec — the exact hazard the singleton's doc warns about.

Fix

Return { ...fallback } on both fallback paths of each normalizer (a shallow copy fully de-aliases since every field is a primitive), mirroring how normalizeNetworkAllowlist / normalizeEcosystemList already copy their fallback on every return path. No production values change; DEFAULT_AMS_POLICY_SPEC and its sub-objects stay frozen.

Tests

Added to packages/loopover-engine/test/ams-policy-spec-parser.test.ts:

  • Parsed capLimits/convergenceThresholds are deepEqual to the defaults but notStrictEqual (distinct objects), are not frozen, and mutating them cannot corrupt DEFAULT_AMS_POLICY_SPEC.
  • The not-a-mapping paths ({ capLimits: "nope", convergenceThresholds: [] }, and the configured-field variant that actually returns the normalized spec) also yield fresh, non-aliased copies.
  • DEFAULT_AMS_POLICY_SPEC and all three of its sub-objects remain Object.isFrozen.

The two regression tests fail against the current by-reference code; the existing value assertions pass unchanged. Engine-flag coverage of the four changed lines verified (source-mapped lcov).

Closes #9995

@RealDiligent
RealDiligent requested a review from JSONbored as a code owner July 31, 2026 06:09
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-31 06:33:04 UTC

2 files · 1 AI reviewer · no blockers · readiness 98/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
Fixes a real aliasing bug: normalizeCapLimits and normalizeConvergenceThresholds previously returned the frozen DEFAULT_AMS_POLICY_SPEC sub-objects by reference on the undefined/null and not-a-mapping paths, while only the valid-mapping path built a fresh object. The shallow `{ ...fallback }` copy is correct since both AmsCapLimits and PortfolioConvergenceThresholds are flat primitive-only shapes, and this mirrors the existing pattern already used in normalizeNetworkAllowlist/normalizeEcosystemList/normalizeExtraHosts in the same file. Tests directly exercise both fallback branches (undefined/null and not-a-mapping) plus the previously-untested case where a configured sibling field routes through the not-a-mapping fallback into the returned (non-default) spec, and assert frozen-state is preserved on the singleton.

Nits — 2 non-blocking
  • The new inline comments in normalizeCapLimits/normalizeConvergenceThresholds are a bit verbose for a one-line fix; a shorter note like the existing normalizeEcosystemList comment would keep style consistent (ams-policy-spec.ts:211-213, :232-233).
  • Consider trimming the two new multi-line comments to a single line to match the terser style of the comment above normalizeNetworkAllowlist's fallback returns.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9995
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 242 registered-repo PR(s), 94 merged, 38 issue(s).
Contributor context ✅ Confirmed Gittensor contributor RealDiligent; Gittensor profile; 242 PR(s), 38 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The diff makes both normalizeCapLimits and normalizeConvergenceThresholds return fresh shallow copies on all fallback paths (undefined/null and not-a-mapping), matching the exact pattern used by normalizeEcosystemList/normalizeNetworkAllowlist as required, and adds regression tests verifying values are unchanged, objects are non-aliased/unfrozen, and DEFAULT_AMS_POLICY_SPEC remains frozen.

Review context
  • Author: RealDiligent
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 242 PR(s), 38 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Keep the PR focused and include validation evidence before maintainer review.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

…POLICY_SPEC sub-objects

DEFAULT_AMS_POLICY_SPEC is a deep-frozen shared singleton, but normalizeCapLimits and
normalizeConvergenceThresholds returned their fallback (DEFAULT_AMS_POLICY_SPEC.capLimits /
.convergenceThresholds) BY REFERENCE on the undefined/null and not-a-mapping paths. So a
parsed spec that carries at least one configured field — and therefore returns the
normalized spec rather than the all-defaults clone — aliased the frozen singleton into a
caller's mutable spec, exactly the hazard the singleton's own doc comment ("clone before
layering overrides") warns about.

Return { ...fallback } on both fallback paths of each normalizer, mirroring how
normalizeNetworkAllowlist / normalizeEcosystemList already copy their fallback. All parsed
VALUES are byte-identical; DEFAULT_AMS_POLICY_SPEC and its sub-objects stay frozen.

Closes JSONbored#9995
@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.88%. Comparing base (a7673e2) to head (f116abf).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10069      +/-   ##
==========================================
- Coverage   91.88%   91.88%   -0.01%     
==========================================
  Files         930      930              
  Lines      113827   113832       +5     
  Branches    27466    27466              
==========================================
+ Hits       104588   104589       +1     
  Misses       7940     7940              
- Partials     1299     1303       +4     
Flag Coverage Δ
backend 95.66% <100.00%> (-0.01%) ⬇️
engine 72.35% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-engine/src/ams-policy-spec.ts 98.12% <100.00%> (+0.02%) ⬆️

... and 1 file with indirect coverage changes

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

engine(miner): stop parseAmsPolicySpec handing out the frozen DEFAULT_AMS_POLICY_SPEC sub-objects by reference

1 participant