Skip to content

fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable - #30448

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-3koeqm
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-3koeqm

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#2 · os-bill · session_01Utp6CZjAtpVuoYZBzB9n5i

Fixes #30385

Clause-②: yes (widening) — SettingsService.setMany / set (and PUT /api/settings/:namespace) accept a clear (null / undefined) of a key an OS_* override in force pins, where every write to such a key was refused 409 SETTINGS_LOCKED; every other write is still refused. No export, type or error code changes.
Metadata-protocol: none

维护者速读

  • 改了什么:环境变量钉住的设置项(如 OS_AUTH_ALLOWED_IP_RANGES)现在可以“清除”:清除只清掉它下面存的值,当前生效的仍是环境变量;其他任何写入照旧 409。IP 白名单的拒绝提示改写成完整的四步救援。
  • 为什么改:卡上实测,管理员被自己的 IP 白名单锁在外面后,按提示把变量设空能进来,但存着的旧列表清不掉(保存一律 409),一去掉变量重启又被锁,文档写的救援办法只管一次。
  • 风险与回滚:放宽,只放行“清除”这一种写入;行锁、平台档、写入谓词照旧拦。依赖维护者对 ADR-0007 第 6 条(env 锁定拒绝一切写入)的修订,修订未定前席位不落地。回滚即撤本 PR。
  • 先看:packages/services/service-settings/src/settings-service.ts 的 setMany 预检里 env 锁那一处分支;packages/plugins/plugin-auth/src/allowed-ip-ranges.ts 四种语言的恢复句;packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts 末尾的端到端用例;content/docs/permissions/authentication.mdx 的锁定恢复条目(已改成四步)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-auth exec vitest run src/allowed-ip-ranges.pin.test.ts,预期 1 个文件、10 条全绿(最后一条是 env 置空 → 清除 → 去掉变量重启后仍能进)。
  • 需要你做的:裁定 ADR-0007 第 6 条的修订(席位已提交),之后席位落地。

What changes

The rule: an OS_* override in force refuses every write to the key it pins except a clear, and a clear clears the value stored beneath it.

  • The env-lock branch (@objectstack/service-settings, setMany's pre-flight). It threw SettingsLockedError (locked-by-env) whenever effectiveEnvOverride was in force, whatever the patch value. A clear (null / undefined, the spelling every clear in this service uses, never a value such as '' that a key would store) now goes on to the judgments any clear meets: the Phase 2 row locks (lockThatRefuses), the platform-rung rule, the write predicates. The write then nulls the stored row as every clear does (the audit records a reset), so the key falls back to its default once the override is removed. Nothing in force changes: the answer still reads the env value with source: 'env' and locked: true. No change to effectiveEnvOverride, auditEnvOverrides, validatePatch, the routes or any export.
  • Why the stored value needed a door. The override hides the stored row from every read and refused every write to it, so an operator who pinned a key to recover from a bad stored value got that value back the moment the pin was lifted. For auth.allowed_ip_ranges that made the documented lockout recovery hold only while the variable stayed set.
  • The recovery copy (@objectstack/plugin-auth, allowed-ip-ranges.ts). The save refusal's recovery sentence, in en, zh-CN, ja-JP and es-ES, now names the four steps that work: start the server with OS_AUTH_ALLOWED_IP_RANGES set empty, sign in, clear the stored list with a settings request that sets allowed_ip_ranges to null (PUT /api/settings/auth), then remove the variable. It names the settings API, not Setup, because the console renders a pinned field read-only and offers no reset on it.
  • Docs. docs/HARDENING.md (the Network row gains the recovery); content/docs/protocol/kernel/config-resolution.mdx (the write-error table: an override in force refuses a write "and the write is not a clear"; added to the claimed surface by the seat); packages/services/service-settings/README.md (Env override: every write but a clear (null) fails with HTTP 409, and a clear removes the value stored beneath the override and changes nothing in force); content/docs/permissions/authentication.mdx (the "If you are already locked out" bullet now walks the four steps, and says that removing the variable without the clear brings the stored list back). The changeset names the earlier allowed-IP entry's recovery sentence it supersedes.

Tests

packages/services/service-settings/src/settings-service.test.ts, new top-level describe (5 cases), on the real SettingsService:

  • a clear under a pin set over a stored value: the answer reads { value: env, source: 'env', locked: true }, a reset change event fires, and once the pin is lifted the key reads its default, not the old value;
  • undefined is a clear too, beside an ordinary write in the same batch;
  • every other write is refused SETTINGS_LOCKED / locked-by-env as before, '' included, and a batch carrying one stores nothing;
  • a locked row at an upper rung still refuses a clear below it (locked-by-global);
  • a locked row at the write's own rung refuses a non-platform clear (locked-by-global), and the platform administrator's clear goes through.

packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts, new top-level describe (1 case), on the real AuthPlugin, the real SettingsService with the shipped auth manifest and the real settings routes: a stored list that refuses ::1; restart with OS_AUTH_ALLOWED_IP_RANGES='' admits it; '' through PUT /api/settings/auth is 409 SETTINGS_LOCKED (details exact); null is 200 with the value read from env, locked, and the gate stays open; remove the variable, restart: ::1 is admitted.

Suites and typecheck (head 173cc44)
  • pnpm --filter @objectstack/service-settings test: Test Files 68 passed (68), Tests 1161 passed (1161); typecheck: exit 0.
  • pnpm --filter @objectstack/plugin-auth test: Test Files 224 passed (224), Tests 3955 passed | 10 skipped (3965); typecheck (tsc, the examples project and check:test-typecheck): exit 0.
Measured before the fix (base 0e4dfbe, real AuthPlugin + SettingsService + settings routes)
  1. Stored list 203.0.113.0/24, no override: ::1 refused at the gate.
  2. OS_AUTH_ALLOWED_IP_RANGES='', restart: ::1 admitted; the key answers { value: '', source: 'env', locked: true }.
  3. Through PUT /api/settings/auth: '' and null both 409 SETTINGS_LOCKED (locked-by-env); resetNamespace('auth') returns without clearing it (the key resolves env, so the reset skips it).
  4. Remove the override: the key answers the stored list again (source: 'global'); restart: ::1 refused.
Ablations (each through `scripts/ablation-replace.mjs` on `settings-service.ts`: anchor 1 → 0, blob changed, restore blob == HEAD blob, `git diff HEAD` empty; both test files resolve `service-settings` from source, so no build sits between)
  • A. the clear exemption removed (if (/* ABLATION-30385-A */ this.effectiveEnvOverride(…))): service-settings 4 failed | 1 passed (the clear, undefined, and both lock cases, which now answer locked-by-env instead of locked-by-global); plugin-auth pin 1 failed | 9 passed (the end-to-end recovery).
  • B. every write let through the pin (if (false /* ABLATION-30385-B */ && …)): service-settings 1 failed | 4 passed (every other write refused); plugin-auth pin 1 failed | 9 passed (the 409 step).
  • C. a clear skipping the Phase 2 row locks (if (clears) continue; // ABLATION-30385-C after the env branch): service-settings 2 failed | 3 passed (both lock cases); plugin-auth pin 10 passed.

Acceptance notes

  • ADR-0007 D6 dependency. ADR-0007 (Accepted) D6 reads "Writes from UI are rejected with HTTP 409 + reason locked-by-env", and its REST table "409 if any key locked-by-env". This change lets a clear through, so D6 needs its amendment (AGENTS.md Prime Directive 13). docs/adr/** is Tier H and outside this change; the seat has put the amendment to the maintainer, and this PR does not land before it. The code comment cites ADR-0007 D6.
  • Setup cannot issue the clear yet. SettingsField disables a locked field and hides "Reset to default" on it, so the clear is a settings API request for now; the console half is the seat's to file. The env answer also lists no stored value beneath the pin, so the console cannot tell whether there is one to clear.
  • content/docs/permissions/authentication.mdx rewritten here. PR docs(permissions): state the allowed_ip_ranges save refusal, the live clear and the env recovery #30382 merged first (2026-11-05T10:10Z, 7d7babb1), adding the "Saving, clearing and lockout recovery" subsection. Its last bullet ("a save to it is refused with 409 SETTINGS_LOCKED. Remove the variable, and the stored list applies again") is false after this change. Seat ruling: whichever lands second rewrites that bullet, so this PR does. It now names the four steps: start with OS_AUTH_ALLOWED_IP_RANGES empty, sign in, PUT /api/settings/auth with allowed_ip_ranges: null, remove the variable.
  • resetNamespace (the reset action) still skips a pinned key. It builds its patch from keys whose resolved source is a stored rung, and a pinned key resolves env, so a row stored beneath a pin survives a namespace reset while the action answers "environment/default configuration is back in effect". Unchanged here (outside the env-lock branch); reported to the seat.

Verification

Suites, typecheck, lint and the full dogfood on head 173cc44e (origin/main merged in; its one conflict was the tail of allowed-ip-ranges.pin.test.ts, where PR #30392 appended its own describe; both are kept). Head 5c0cbbfb adds one docs commit for the contract review's two must-fixes (packages/services/service-settings/README.md, content/docs/permissions/authentication.mdx) and changes no code, so those were not re-run on it. The derived gates were re-run on 5c0cbbfb.

Derived gates: 93 derived, 93 exit 0 (head 5c0cbbf)
  • 5c0cbbfb: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 93 commands for the 10 changed paths (one more than round 1: pnpm check:merge-driver); each ran with its exit code captured before any pipe. --ran reconciliation: "93 derived famil(ies) accounted for — 93 run, 0 NOT-MEASURED". They ran on commit b4b507a9; the pushed 5c0cbbfb carries the same tree (af8723c4), its message reworded only because the pre-push check:commit-card-trailers refused a card trailer.
  • 173cc44e: 92 derived, 92 exit 0, --ran "92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED". check:dual-build-cjs-loads and check-plugin-teardown-shape --self-test exited 3 on the first round (no whole-workspace build; shallow checkout); after building the dogfood closure and fetching 621a4876, both exit 0.
  • pnpm lint (eslint . --no-inline-config): exit 0.
Full dogfood (asked by the order; head 173cc44)
  • Built the closure, then pnpm --filter @objectstack/dogfood test under the verify lock: VERDICT command-exit 0, Test Files 419 passed | 3 skipped (422), Tests 3492 passed | 36 skipped (3528).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic (untouched).
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is Chinese by the Communication rule).

…ored value beneath the pin

An env override in force refused every write, clears included, so the
documented allowed-IP recovery (OS_AUTH_ALLOWED_IP_RANGES set empty) could
not clear the stored list and removing the variable locked the caller out
again. A clear (null/undefined) now goes through the pin; every other write
stays 409 SETTINGS_LOCKED. The refusal copy names the full recovery.

Claude-Session: https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i
Co-authored-by: Claude <noreply@anthropic.com>
# Conflicts:
#	packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/service-settings, touching 4 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/services/service-settings/README.md, packages/services/service-settings/src/settings-service.types.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via SettingsService (symbol, a top-level class))
  • content/docs/kernel/runtime-services/settings-service.mdx (via setMany (symbol, a method of class SettingsService))
  • content/docs/permissions/authentication.mdx (via /api/settings/auth (route, a path literal in ALLOWED_IP_RANGES_REFUSAL_COPY; a path literal in recovery))
  • content/docs/protocol/kernel/config-resolution.mdx (via SettingsService (symbol, a top-level class), setMany (symbol, a method of class SettingsService))
  • content/docs/protocol/kernel/index.mdx (via SettingsService (symbol, a top-level class))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via setMany (symbol, a method of class SettingsService))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/services/service-settings/README.md, packages/services/service-settings/src/settings-service.types.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json adf6ee0948d8dd8e747678e6d5107e0c4a86bb31 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 030502c19f26155dcf2adab46f69a03af554a8a4 — the merge of head 034aee8fc06a88ceb0b8532ab5db81838f8193ad into base adf6ee0948d8dd8e747678e6d5107e0c4a86bb31, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 030502c19f26155dcf2adab46f69a03af554a8a4 && git checkout 030502c19f26155dcf2adab46f69a03af554a8a4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin adf6ee0948d8dd8e747678e6d5107e0c4a86bb31 034aee8fc06a88ceb0b8532ab5db81838f8193ad && git checkout -B drift-repro adf6ee0948d8dd8e747678e6d5107e0c4a86bb31 && git merge --no-ff 034aee8fc06a88ceb0b8532ab5db81838f8193ad

node scripts/docs-audit/affected-docs.mjs --json adf6ee0948d8dd8e747678e6d5107e0c4a86bb31

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs adf6ee0948d8dd8e747678e6d5107e0c4a86bb31 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 173cc44ee855da37932e879685f181625e6102da
Local-runs: none

Face. The committed tree carries no published schema and no governed path; the changeset's line reads Clause-②: yes (widening), so the face is the declaration face and its record token is TIER_DEFAULT (record-recognisers.mjs --template --paths on the eight changed paths). The seat ordered this review run at the contract-review tier (the dispatch order's lock policy); that names who ran it, not the face.

Read. The diff of 173cc44e against its merge base 09720e2c (8 files, +214 / −12), card #30385 with comments 6653992606 / 6654828319 / 6657238833, the PR body, ADR-0007 D6 and its REST table, settings-service.ts on the head (setMany, lockThatRefuses, effectiveEnvOverride, refuseByWritePredicates, validatePatch, resetNamespace, upsertRow, rowIdentity, resolveKeyFromRows, get / getMany), settings-routes.ts (the PUT handler and its 409 mapping), settings-secret-redaction.ts's dropEchoedSecretMasks, both test files, both packages' package.json, the console's SettingsField / SettingsView, and the head's check-runs read at 2026-11-05T12:00Z. Nothing built or run locally.

① Derived judgments

  1. The env-lock branch admits exactly a clear and nothing else. setMany pre-flight on the head: const clears = patch[key] === null || typeof patch[key] === 'undefined'; if (!clears && this.effectiveEnvOverride(reg, namespace, key)) throw new SettingsLockedError(namespace, key);. '', 0, false, [] and every value a key would store still throw locked-by-env (test 3 pins 'Other' and ''; the end-to-end pin answers '' with 409 and exact details). effectiveEnvOverride, auditEnvOverrides, get / getMany (still answer { source: 'env', locked: true } from the override) and the route's 409 SETTINGS_LOCKED mapping are untouched. Correct.
  2. A clear still meets every other judgment. The new guard wraps only the env test. For every key the Phase-2 block runs after it unchanged: lockThatRefuses over the global row and the caller's preferred tenant row — an upper-rung lock refuses every writer, an own-rung lock refuses every writer but the platform rung (writesAsPlatform). validatePatch (an all-null patch returns early — pre-existing), refuseByWritePredicates (runs: a clear under a pin compares null with the env value and calls the key's predicate), and the encryption pre-flight (a clear is exempt — pre-existing) are unchanged. Pinned by tests 4 and 5 (locked-by-global on an upper-rung lock beneath the pin; an own-rung lock refuses orgAdmin and platformAdmin writes through, answering env / locked). Correct.
  3. The stored value really goes. The write loop stores value: null through upsertRow / rowIdentity — the service's existing clear: the row is nulled, not deleted, the audit and the change event record action: 'reset' — and resolveKeyFromRows takes the first non-null chain entry, so once the override is lifted the key reads its default (source: 'default'; test 1, test 2 and the end-to-end pin). The seat's ruling says "deletes the stored row"; the build nulls it, which is what every clear in this service does — equivalent for the recovery. The ADR amendment should say "clears the stored value", not "deletes the row".
  4. Effective value and locked reporting unchanged. The answer of a clear is re-resolved through get, which returns the override first: { value: env, source: 'env', locked: true, lockedReason: 'Set via env: OS_…' } (test 1 toMatchObject and lockedReason containing OS_PINNED_MOTTO; end-to-end { value: '', source: 'env', locked: true }). Correct.
  5. No other write path opens by accident. resetNamespace builds its null patch from keys whose source is global / tenant / user; a pinned key resolves env, so a namespace reset and the built-in reset action still skip it — unchanged (the dev reports the consequence under out-of-scope findings; see ③). A batch mixing a pinned key's clear with an ordinary write of another key is accepted (test 2) — per-key judgment, as every unpinned clear already was; a batch carrying a non-clear of the pinned key is refused whole and stores nothing (test 3). undefined is a clear at the service surface (test 2); over REST it cannot arrive — JSON carries no undefined, and dropEchoedSecretMasks drops an echoed key rather than setting it — so PUT /api/settings/:namespace admits exactly null.
  6. The four-locale refusal copy now names a recovery that works. en / zh-CN / ja-JP / es-ES each name the same four steps: start with OS_AUTH_ALLOWED_IP_RANGES set to an empty value, sign in, a settings request that sets allowed_ip_ranges to null (PUT /api/settings/auth), then remove the variable. The end-to-end pin walks exactly those steps on the real AuthPlugin, the shipped auth manifest and the real settings routes (registerSettingsRoutes), ending with ::1 admitted after the variable is removed and the plugin re-booted. The copy names the API rather than Setup because the console disables a locked field (SettingsField: disabled = Boolean(locked || saving), no reset affordance on a locked row) — true on the head.
  7. The two docs edits are true. docs/HARDENING.md Network row: the same four steps. content/docs/protocol/kernel/config-resolution.mdx:221: "…and the write is not a clear" — the row-lock row beneath it still applies to a clear, which matches the code (item 2). Both sentences checked against the diff and the tests.
  8. Published prose this diff makes false — must-fix. packages/services/service-settings/README.md §Env override: "Such fields are returned with source: 'env', locked: true and writes (service or REST) fail with HTTP 409." The README ships in the npm tarball (files: ["dist", "README.md", "CHANGELOG.md"]), and after this diff a clear does not fail. The checklist's rule: a published sentence this round makes false is fixed in this round. Rewrite to: every write but a clear (null) fails with HTTP 409; a clear removes the stored value beneath the override and changes nothing in force.
  9. content/docs/permissions/authentication.mdx is untouched — and the coordination condition now puts the rewrite on this PR; must-fix. The seat's ruling: whichever of PR docs(permissions): state the allowed_ip_ranges save refusal, the live clear and the env recovery #30382 and this PR lands second rewrites the page's last recovery bullet to the four steps. PR docs(permissions): state the allowed_ip_ranges save refusal, the live clear and the env recovery #30382 merged at 2026-11-05T10:10:39Z (7d7babb1), before this PR's second merge of origin/main and before it opened, and the bullet sits in this PR's own tree (authentication.mdx:676-684): "…the field is locked: a save to it is refused with 409 SETTINGS_LOCKED. Remove the variable, and the stored list applies again at the next restart." After this diff the first sentence is false for a clear (the page's own previous bullet defines a clear as "empty the field and save", and the console spells that as null), and the bullet still describes the loop the card exists to close; the card's second Wanted item names this page. The PR body ("PR docs(permissions): state the allowed_ip_ranges save refusal, the live clear and the env recovery #30382 (open, another lane)") and the report's deviations are stale on the head's own tree. Rewrite the bullet to the four steps and correct the body sentence.
  10. Pins and ablations are plausible against the diff. A (exemption removed) must fail the clear, the undefined clear and both lock cases (they answer locked-by-env instead of locked-by-global) plus the end-to-end recovery — reported 4 | 1 and 1 | 9. B (every write through the pin) must fail only "every other write refused" and the end-to-end 409 step — 1 | 4 and 1 | 9. C (a clear skipping the Phase-2 block) must fail both lock cases and nothing end-to-end — 2 | 3 and 10 pass. Each count is what those assertions do. Taken from the report; not re-run here.

② Semver level

  • Clause-②: yes (widening) is the right declaration: the accept set of SettingsService.setMany / set and of PUT /api/settings/:namespace widens (a request refused 409 now succeeds), and the documented error contract moved (set() and setMany() docblocks, the SettingsLockedError docblock, the config-resolution.mdx table — and the README of item ①8). No export, type or error code changes: index.ts untouched, SETTINGS_LOCKED still in the ledger with the same reason values. Metadata-protocol: none is right — nothing an author writes changes. Changeset .changeset/30385-clear-env-locked-setting.md: @objectstack/service-settings patch, @objectstack/plugin-auth patch — none takes patch and a widening ships patch; no BREAKING banner and no ADR-0087 marker owed (no narrowing, no removal). Both declaration lines stand at line start in the body and the changeset; Check Changeset green on the head.
  • Both packages publish (private unset); both carry the changeset. The changeset's note that it supersedes the earlier allowed-IP entry's recovery sentence is the right carrier for that correction (no CHANGELOG edit). The README fix (①8) rides the same patch.

③ Boundary flags

  • Landing depends on the maintainer's ruling on ADR-0007 D6 and on a separate Tier H amendment of ADR-0007. D6 (Accepted) reads "Writes from UI are rejected with HTTP 409 + reason locked-by-env" and the REST table "409 if any key locked-by-env"; this diff makes a clear succeed, which contradicts the ADR (AGENTS.md Prime Directive 13: a new ADR or an amended status line, never a changeset that quietly does the opposite). The amendment is not in this PR (docs/adr/** is Tier H and outside the ordered surface) and the ruling is pending among A (this shape), A′ (single-key null only; namespace reset keeps skipping pinned keys), B (an out-of-band CLI clear) and C (copy only). The PR body says so: the 维护者速读 risk line ("依赖维护者对 ADR-0007 第 6 条…修订未定前席位不落地"), its "需要你做的" line, and the Acceptance note "ADR-0007 D6 dependency … this PR does not land before it". The code comment cites ADR-0007 D6 (Prime Directive 13's "leave its id in the code"); no scripts/adr-anchors/ file — add one with the amendment.
  • This review judges the PR as implementing shape A. Compatibility with A′ as built: (a) per key, a pinned key admits only a clear and nothing else — holds; (b) namespace reset and the reset action keep skipping pinned keys — holds, unchanged (the dev lists the consequence as out of scope, class a: the reset action answers "environment/default configuration is back in effect" while a row beneath a pin survives — under A the seat files it; under A′ it is by design and resetNamespace's docblock, "env wins over rows anyway and refuses writes", should say so — that docblock is half-true now either way, left per the seat because another card edits nearby); (c) two points A′ might read more narrowly than the build: undefined counts as a clear at the service surface (REST admits only null), and a pinned key's clear may ride in a multi-key batch beside ordinary writes of other keys (test 2). If A′ means "the pinned key takes only null", the PR is A′ already; if it means "a one-key null patch and nothing else", the tightening is a few lines in the same pre-flight plus flipping test 2 — no structural change. B and C are not this PR.
  • Setup cannot issue the clear today (SettingsField disables a locked field; the env answer's cascade shows nothing beneath the pin) — the dev's carrier note to the seat for the domain:ui card. For that card: the console already spells an emptied text field as null (textOrUnset), so enabling the clear needs no new spelling, only a server read that says whether a stored value lies beneath the pin.
  • No open_questions; security_findings: none; premise_still_valid: true, premise measured on 0e4dfbe2 (the report's four steps). Author objectstack-fleet[bot] through the relay; no reviewer or assignee on the PR. No model name in the body, the commits (the model-free trailer pair) or the changeset. Surface held: nothing under ui/**, apps/**, packages/spec/**, skills/**, docs/adr/**, .claude/**, content/docs/releases/**; config-resolution.mdx was added to the surface by the seat; settings-service.types.ts carries a docblock only.
  • Check-runs on 173cc44e at the reading above: 49 runs — 34 success, 12 skipped (UI jobs and opt-in harnesses, expected), 3 in_progress: Test Core (1/8), Test Core (4/8), Test Core (6/8). Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Test UI, Live E2E UI, Check Changeset and the other five Test Core shards are success. Not green yet; the next head carries its own set.

Must-fix (two docs edits, one push; the new head gets a delta review):

  1. packages/services/service-settings/README.md §Env override — the "writes … fail with HTTP 409" sentence (①8).
  2. content/docs/permissions/authentication.mdx "If you are already locked out" bullet → the four steps, and the PR body's "docs(permissions): state the allowed_ip_ranges save refusal, the live clear and the env recovery #30382 (open, another lane)" → merged (①9).

Non-blocking: ①3 wording for the amendment ("clears the stored value"); ③ reset-action finding filed or declared by design once A / A′ is ruled; resetNamespace docblock; ADR anchor; the ui card note.

Implemented-by: session_01Utp6CZjAtpVuoYZBzB9n5i
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: FAIL

…overy is four steps

- service-settings README, Env override: every write but a clear (null)
  fails with HTTP 409; a clear removes the value stored beneath the
  override and changes nothing in force.
- authentication.mdx, "If you are already locked out": PR #30382 merged
  first, so this PR rewrites the bullet (seat ruling: the later lander
  rewrites it) to the four steps that work: start with
  OS_AUTH_ALLOWED_IP_RANGES empty, sign in, PUT /api/settings/auth with
  allowed_ip_ranges null, remove the variable. The forward reference
  above it now says "steps".

Claude-Session: https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review — delta

Served-tier: TIER_DEFAULT
Head-sha: 5c0cbbfb96
Local-runs: none
Scope: delta 173cc44e → 5c0cbbfb (one commit, docs only: packages/services/service-settings/README.md, content/docs/permissions/authentication.mdx) and the PR body, against the two must-fix items of record 6657453112.

① Derived judgments

  1. Must-fix 1 (README §Env override) — fixed. "Every write (service or REST) but a clear (null) fails with HTTP 409. A clear removes the value stored beneath the override and changes nothing in force" matches setMany's pre-flight on the head (only null / undefined pass the env test; REST carries only null) and get answering the override first. "falls back to its default once the variable is removed" holds for the rung that was cleared; a row on another rung, if any, still resolves first — a reading of the README's own resolver line above it, not a false sentence.
  2. Must-fix 2 (authentication.mdx locked-out bullet) — fixed. The bullet is now the four steps (empty OS_AUTH_ALLOWED_IP_RANGES → sign in → PUT /api/settings/auth with { "allowed_ip_ranges": null } → remove the variable and restart), the same steps as the four-locale refusal copy and the end-to-end pin. Step 3 says Setup shows the field read-only (true: SettingsField disables a locked field) and that the clear changes nothing in force (true: the answer still reads env). The closing sentence ("Removing the variable without step 3 brings the stored list back") states the loop the card closes. "recovery step" → "recovery steps" in the bullet above follows.
  3. PR body — fixed. The Acceptance note now records PR docs(permissions): state the allowed_ip_ranges save refusal, the live clear and the env recovery #30382 merged first (10:10Z, 7d7babb1) and this PR rewriting the bullet per the seat ruling. No "(open, another lane)" left.
  4. No code, test, changeset or declaration line moved; ① 1–7 and 10 of record 6657453112 carry forward unchanged.

③ Boundary flags

  • Landing still depends on the maintainer's ADR-0007 D6 ruling and a separate Tier H amendment (record 6657453112 ③). Unchanged; the PR stays draft.
  • Check-runs on 5c0cbbfb at this reading: 12 success, 14 skipped, 17 in progress, 0 failure.

Implemented-by: session_01Utp6CZjAtpVuoYZBzB9n5i
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

Must-fix: none. Landing gate: ADR-0007 D6 ruling + Bill's word.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Progress · 2026-11-05T16:10Z — waiting on a maintainer ruling, not idle work.

Reading Value
Head 5c0cbbfb; seat delta review PASS 6658228926
Blocked on the maintainer's ruling on the ADR-0007 D6 question this PR raises (Tier H), presented through the seat's decision thread
Next step on the ruling: apply it (or none), merge main, re-run the checks, seat ACCEPT, then pr_ready + automerge through the relay
ETA the ruling; no code moves before it

The one conflict is the tail of allowed-ip-ranges.pin.test.ts (and its
header list), where #30398's address-read describe landed beside this
branch's #30385 describe; both are kept, no behaviour changed.

Claude-Session: https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Delta (merge of main) · 2026-11-05T17:04Z · head 4153daa426dfe5c6c5413c815e56b97e4bc6de53 (from 5c0cbbfb, delta PASS 6658228926)

No conflicts; no behaviour changed.

Claude-Session: https://claude.ai/code/session_01Utp6CZjAtpVuoYZBzB9n5i
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

席位核对 — ACCEPT · domain:server#2 · head 034aee8fc06a88ceb0b8532ab5db81838f8193ad

  • Shape: draft, author objectstack-fleet[bot] (relay pr_create), no assignee. 10 files, +238 −24 against main today. check-governed-merges --pr 30448: NOT governed, 262 lines. The title has no !. Fixes #30385. Clause-②: yes (widening), Metadata-protocol: none. No merge=os-regen path.
  • Premise measured: on main, the setMany pre-flight refused any value under an env override in force. The PR lets only null / undefined through. A clear nulls the row (upsertRow) and never deletes it. The effective answer stays source='env', locked=true.
  • Review: contract review FAIL 6657453112 (two docs must-fixes), fixed in 5c0cbbfb; delta PASS 6658228926. Since then the head has only merged main (4153daa4 → f31bebbb → this head, the last through the platform's update-branch). Ruling 6670154789 order is met: ADR-0007 D6 amendment docs(adr): ADR-0007 D6 — a null clear of an env-pinned key is not refused (#30385) #30698 merged first.
  • Dev readings:
  • 门禁 check-runs: in progress on this head at ACCEPT. Auto-merge waits for every required check.
  • Out of scope: reset (Settings reset: an env-pinned key keeps its stored value beneath the pin, while the reset says the namespace is back on environment/default configuration #30664). The ui clear control is a domain:ui card, Blocked-by: #30385, owed per ruling item 5. Skills-docs: none.
  • 落地条款: pr_ready + automerge_enable on this head. On merge: pm:dispatched off, landing comment.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant