fix(engine): deep-copy and freeze the default command-authorization policy - #10137
Conversation
…olicy normalizeCommandAuthorizationPolicy's two exit paths disagreed about ownership of the returned object. The non-record path deep-copies via clonePolicy; the record path seeded commands with a SHALLOW spread of DEFAULT_COMMAND_AUTHORIZATION_POLICY, so every un-overridden command's role array was the SAME instance the module-level default holds. Only explicitly-overridden commands got a fresh array. A caller that pushed a role through a returned policy would widen that command for every repo in the same isolate, permanently, with no config change and no audit trail -- the default is the security vocabulary for the whole command surface. Seed the record path from clonePolicy too, so every returned role array is freshly allocated on every input, and freeze DEFAULT_COMMAND_AUTHORIZATION_POLICY (object, default array, commands record, and every role array) so a future aliasing regression fails loudly instead of silently corrupting the vocabulary. Values, warnings, key validation, the maintainer-only clamp, and every evaluateCommandAuthorization decision are unchanged. Closes JSONbored#9998
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-31 09:18:19 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk 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.
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.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10137 +/- ##
==========================================
+ Coverage 91.99% 92.05% +0.05%
==========================================
Files 931 931
Lines 113994 114008 +14
Branches 27523 27534 +11
==========================================
+ Hits 104871 104945 +74
+ Misses 7823 7759 -64
- Partials 1300 1304 +4
Flags with carried forward coverage won't be shown. Click here to find out more.
|
What & why
normalizeCommandAuthorizationPolicy's two exit paths disagreed about who owns the returned object.The non-record path deep-copies via
clonePolicy(copiesdefaultand every command's role array). The record path did not:That shallow spread makes every value the same array instance held by the module-level
DEFAULT_COMMAND_AUTHORIZATION_POLICY(a plain exported const, not frozen). Only commands the caller explicitly overrode get a fresh array; every un-overridden command keeps the shared reference. So this held today:The function is reached with a record on the live settings-write path and on every command-authorization read, so the aliased arrays are handed to real callers.
DEFAULT_COMMAND_AUTHORIZATION_POLICYis the security vocabulary for the whole command surface — a singlepushthrough a returned policy would widen that command for every repo handled by the same isolate, permanently, with no config change and no audit trail. No current caller mutates them, which is why this is a latent correctness defect rather than a live incident.The fix (both halves required)
commandsfromclonePolicy(DEFAULT_COMMAND_AUTHORIZATION_POLICY).commands— the same deep copy the non-record exit already returns — so every returned role array is freshly allocated on every input, including un-overridden commands.DEFAULT_COMMAND_AUTHORIZATION_POLICY, itsdefaultarray, itscommandsrecord, and every role array inside it areObject.freezed, so a future aliasing regression fails loudly (a strict-modeTypeErroron the offendingpush) instead of silently corrupting the vocabulary.Unchanged: the resolved role lists for every input (
{}still produces acommandsdeep-equal to the default and a deep-equaldefault), every warning string, the/^[a-z][a-z-]{0,63}$/key validation, the maintainer-only clamp, and everyevaluateCommandAuthorizationdecision.Tests
packages/loopover-engine/test/command-authorization.test.ts, new):{}returns role arraysnotStrictEqualto the default's yetdeepEqual; the two exit paths agree (nulland an override both return non-aliased arrays for an un-overridden command likepause); mutating a returned array doesn't change the frozen default; and the preserved behaviour (commandAuthorizationAllowedRoles(null, "generate-tests")is["maintainer"], a COLLABORATOR is unauthorized forgenerate-tests).test/unit/command-authorization-engine.test.ts, the vitest mirror that imports the engine src for codecov): the same non-aliasing + freeze assertions across{},null, and an override.mainand pass with the fix.Validation
packages/loopover-engine/src/settings/command-authorization.tsis 100% line and branch (the freeze loop and the deep-copy seed). Engine lines credited via the root-vitest upload; the added test is also inpackages/loopover-engine/test/**for the dual-upload union.npm run typecheckclean for these files;npm run engine-parity:drift-checkpasses; the engine's ownnode --testsuite (928 tests) is green;npm run dead-exports:checkclean; both existing command-authorization suites still pass.git diff --checkclean; no schema/migration/generated-artifact change.Closes #9998