Repository navigation
fix(service-settings): a namespace reset clears the value stored beneath an env pin - #30793
Conversation
…ath an env pin resetNamespace skipped every key an OS_* override in force pins, so a value stored beneath the pin survived the reset and came back when the variable was removed. It now reads the stored cascade beneath the pin and clears that value with the same null write setMany lets through a pin, which changes nothing in force. The count includes those keys, and the built-in reset action adds one sentence naming how many keys the environment still pins, in all eleven languages. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cytLmJxfAmgPVjUbbW
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3d45e193d45cbd1291c0a860d7b1ebf1f738b7f3 && git checkout 3d45e193d45cbd1291c0a860d7b1ebf1f738b7f3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 612468cc20e4d4adeba047d2416126ec9a8cc077 9b806cbed6ea46f4746e580027316b3890becd4d && git checkout -B drift-repro 612468cc20e4d4adeba047d2416126ec9a8cc077 && git merge --no-ff 9b806cbed6ea46f4746e580027316b3890becd4d
node scripts/docs-audit/affected-docs.mjs --json 612468cc20e4d4adeba047d2416126ec9a8cc077
|
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW 席位核对 · ACCEPT · 2026-11-06T03:39Z · #30664Head-sha: 9b806cb
|
Fixes #30664
Clause-②: no
Metadata-protocol: none
维护者速读
packages/services/service-settings/src/settings-service.ts的resetNamespaceRows(读钉住项下面的存值)与runAction的 reset 分支;settings-action-copy.ts新增的resetStillPinned;settings-service.test.ts末尾的新 describe。pnpm --filter @objectstack/service-settings exec vitest run src/settings-service.test.ts -t 'beneath an env pin',预期 5 条全绿。What changes
The rule: a namespace reset clears every stored value it can reach, the one beneath an env pin included, and says how many keys the environment still pins.
resetNamespace(@objectstack/service-settings). It built its patch from keys whose resolved source is a stored rung (global/tenant/user). A key anOS_*override in force pins resolvesenv, so a value stored beneath the pin was never cleared and took over again once the variable was removed, while the built-in reset said "environment/default configuration is back in effect". For a key that resolvesenv, the reset now reads the stored cascade beneath the pin (resolveStored, the half ofget()below the env branch, extracted rather than copied, soget()is unchanged) and, when that names a stored rung, puts the key in the same all-nullpatch. The patch goes throughsetMany, whose env-lock branch already lets a clear through a pin since PR fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 (ADR-0007 D6 as amended), so there is no second bypass of the env lock: the clear meets the same row locks, platform-rung rule and write predicates a single clear meets. Nothing in force changes: the key still answers{ source: 'env', locked: true }. A pinned key with nothing stored beneath it is not in the patch, so it writes no row and no audit entry. The count it returns includes the pinned keys it cleared; its signature is unchanged.settings-action-copy.ts).runAction's built-inresetkeepsresetCleared/resetNothingas they were (their wording is true now that nothing stored survives the reset) and, when some keys are still pinned, appends one new sentence,resetStillPinned(count), in all eleven languages: "2 setting(s) remain pinned by environment variables." Each sentence carries its own leading space where its language separates sentences with one (none in zh-CN, zh-Hant, ja-JP). A namespace no variable pins gets exactly the message it got before.setMany's write loop like any clear, so it records exactly what a single clear of that key records: oneresetrow on each ledger (newHash: null) and oneresetchange event. Pinned in a test that compares the two writer rows field for field.Tests
packages/services/service-settings/src/settings-service.test.ts, new top-level describe (5 cases), on the realSettingsService, with one key pinned over a stored value, one stored and not pinned, and one pinned with nothing stored:resetfor those two keys only, both pinned keys still answer env and locked, and once the variables are removed both read their default;set(…, null);One existing case was renamed to what it pins now ("leaves an env-locked key with nothing stored beneath it untouched"); its assertions are unchanged.
settings-copy-locale.test.tsgets a sample argument forresetStillPinned, so its existing every-language checks cover the new sentence.Suites and typecheck (head 9b806cb)
pnpm --filter @objectstack/service-settings test: Test Files 70 passed (70), Tests 1206 passed (1206).typecheck(tsc --noEmit, includessrc/**/*.test.ts): exit 0.pnpm --filter @objectstack/plugin-auth exec vitest run src/allowed-ip-ranges.pin.test.ts src/auth-settings-clear.pin.test.ts(the two callers ofresetNamespaceoutside the package, after building their closure): Test Files 2 passed, Tests 39 passed.Ablations (each through `scripts/ablation-replace.mjs` on `settings-service.ts`: anchor 1 → 0, blob changed, restore blob == HEAD blob, `git diff HEAD` empty; the tests import the service from source, so no build sits between)
const stored = v.source; /* ABLATION-30664-A */): 3 failed | 2 passed (the count and default-after-unpin case, the audit comparison, the action message); the two that pass pin what must not change (nothing stored beneath a pin; no pins at all).message: answer, /* ABLATION-30664-B */): 1 failed | 4 passed (the action message).Acceptance notes
setManyrefuses the whole resetSETTINGS_LOCKED(locked-by-global/locked-by-tenant) for a caller below that rung, exactly as it already did for an unpinned key in that position. That is the clear's own rule from PR fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448, not a new one.nullat the key's own rung for this caller; a value stored only at a higher rung the caller cannot write stays, and the key falls back to it once the pin goes. Unchanged behaviour, now applied to pinned keys as well.messageis the toast text the console prints and the HTTP action route returns; it gains one sentence only when keys are still pinned. No export, type, error code or route changes;resetNamespacekeeps its signature.SettingsFieldstill hides "Reset to default" on a pinned field; the namespace-level reset now reaches the value beneath it. Nothing inui/changes here.Verification
All readings on head
9b806cbe(branched fromorigin/main1371b9d8).Derived gates: 60 derived, 59 exit 0, 1 NOT MEASURED
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 60 commands for the 5 changed paths; each ran with its exit code captured before any pipe.--ranreconciliation (recorded ascommand :: exit N): "60 derived famil(ies) accounted for — 59 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET (it needs a whole-workspace build; only the closures ofservice-settingsandplugin-authwere built here). CI'sLint & Repo Gatescarries it.pnpm lint(eslint . --no-inline-config): exit 0.Generated by Claude Code