Uh oh!
There was an error while loading. Please reload this page.
feat(settings,auth): expose the audience posture in the auth settings namespace - #12301
Conversation
… namespace
Three new authorable keys in an audience group on the auth namespace
(audience_posture select over the closed vocabulary, a domain-list
textarea, a permission-set name field), mapped by bindAuthSettings to one
applyConfigPatch({ audience }) that replaces the whole audience object and
validates the merged result. Explicit-only application; off-vocabulary
postures refused loudly and never coerced (#5152 precedent); every #11739
invariant holds through the settings channel and refusals fail closed.
Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa
Co-authored-by: Claude <noreply@anthropic.com>…th-audience-settings-surface
…e the visibility-corpus pin The three gated locale bundles (zh-CN / ja-JP / es-ES) gain the audience group and its three keys (en updated for parity); the #7169 visibility corpus tripwire moves 94 -> 96 for the two new posture-gated predicates. Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check7 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 14 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 1b5f4cc0e6637838367dec223a0283ae12593db6 && git checkout 1b5f4cc0e6637838367dec223a0283ae12593db6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5ce5f8c12479f40d548b9f3ba9f8725c14c9bf9a eb316a452e1d3d3d88fd43334a490b54409f2b3b && git checkout -B drift-repro 5ce5f8c12479f40d548b9f3ba9f8725c14c9bf9a && git merge --no-ff eb316a452e1d3d3d88fd43334a490b54409f2b3b
node scripts/docs-audit/affected-docs.mjs --json 5ce5f8c12479f40d548b9f3ba9f8725c14c9bf9a |
os-trump
commented
Aug 25, 2026
Contract-review verdict: PASS (#11768)Reviewed by the contract-review chain at Reviewed: the binding and manifest patches in full, the epic lineage (#11723 ruled A; #11739's invariants; the #5152 precedent), and the seat's ACCEPT with its quoted The Clause-② question — can the settings door reach a posture the config door could not, or bypass a ruled invariant? — answers no, at three independent layers: the dev's measurement, the seat's own code read (the patch replaces the whole The contract shape is right, and the hard direction was reasoned:
Landing completed by the chain in the same stroke per the maintainer's standing instruction (2026-08-25, verbatim: 「审核通过你应该直接负责合并吧,还要等原始的项目经理吗」): clearing Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11768
Part of #11723
The audience posture shipped by #11739 (PR #11767) was switchable only from stack config at boot; the open-source operator surface had no channel. The
authsettings namespace now carries anaudiencegroup — a select over the closed vocabulary (invite_only | email_domain | open), a domain-list textarea, and a permission-set-name field — andbindAuthSettingsmaps the three keys, one atomic declaration, to oneAuthManager.applyConfigPatch({ audience }). The patch replaces the whole audience object and validates the MERGED result (assertAudienceConfig, the same validator the constructor runs), so the settings path cannot reach a posture the config path could not, and every #11739 ruled invariant holds through the new door. Verified against the merged ref, not the card body: the card's premise was re-derived on8064e6da1(post-#11767, post-#12210).The
membership_policyprecedent (#5152), followed exactlyaudienceis never masked by the manifest'sinvite_onlydefault (pinned).isAudiencePostureat the binding, before the value ever reaches the patch; the refusal names the value, the standing posture that keeps ruling, and the vocabulary. The write API refuses off-table values too (SETTINGS_VALIDATION+invalid_optiononsetMany, pinned with the coded envelope), and theOS_AUTH_AUDIENCE_POSTUREenv door is judged against the same option table byeffectiveEnvOverride— with the binding'sisAudiencePosturebehind it as the boundary check the card requires. No new env channel was added; the generic settings env-override door covers these keys by construction.applyConfigPatchis caught insideapplySettings(never escapes the subscribe callback) and reported aterrorlevel with the validator's remedy-bearing message; the standing config keeps ruling.Design decisions worth review (Clause-② surface)
applyConfigPatch({ audience })call, applied AFTER the main settings patch. The card sketched "catch and report per the existing applySettings catch"; the dedicated inner catch is that same path (insideapplySettings, the outer catch remains the net) but reports aterrorwith the audience-specific message —warnis the level the membershipPolicy 无法作为平台设置配置,且注册路径与回填路径读的是两个来源 #5152 precedent explicitly argues against for this class ("nothing looks broken afterwards"). Ordering matters: applying audience last makes the merged-result validation judge it against theemailAndPasswordstate the same pass just applied, sorequire_email_verification: false+ postureopenin one save is refused as the contradiction it is (pinned). The separate call also bounds the blast radius: a refused audience declaration cannot block session/lockout/rate-limit settings riding the same namespace (pinned).invite_onlysends{ posture: 'invite_only' }alone, so leftover text in the posture-hidden domain-list field can never make CLOSING the wall refusable — that refusal would leave the previous, more open posture ruling, the one direction that must not fail (pinned). Missing required siblings are never guessed: the patch goes out without them and the validator refuses (refusing to OPEN fails closed). A sibling declared with no explicit posture is refused loudly, not completed.applyConfigPatchrunsassertAudienceConfig(next.audience, next.emailAndPassword)whenever either key is patched (auth-manager.ts), the same assertion the constructor runs at entry — the settings channel converges on the same validator and the samegetAudience()accessor the admission gate reads. No bypass exists; nothing here loosens the boundary, so the card implements rather than stops.Verification (all at
eb316a452, tree clean; heavy commands throughscripts/pm/os-verify-lock.sh, verdict lines quoted)pnpm --filter @objectstack/plugin-auth test—Test Files 80 passed (80)/Tests 1631 passed (1631);typecheck—VERDICT command-exit 0.pnpm --filter @objectstack/service-settings test—Test Files 29 passed (29)/Tests 518 passed (518);typecheckexit 0.plugin-auth/src/audience-posture-setting.test.ts(the refusal set: empty domain list underemail_domain, missing permission set,admin_full_access, verification contradiction, off-vocabulary, sibling-without-posture; plus explicit-only masking, live re-apply, fail-closedinvite_onlyswitch-back, blast-radius, and the dangling-name/getAudience()convergence pin — the admission-time 403AUTH_CONFIG_ERRORfor a dangling set is already pinned inaudience-posture.test.ts). 3 inservice-settings/src/manifests/auth.manifest.test.ts(closed three-value select, sibling visibility gating,setManyrefusal with the coded envelope).trapscript; mutations confirmed on disk by marker counts before each run):config.audiencewrite bypassing the validator): injected marker 1/1, deleted form 0/0 →Tests 5 failed | 7 passed (12), e.g.AssertionError: expected 'email_domain' to be 'invite_only'on the empty-domain-list pin — the suite can tell an enforcing binding from a pass-through.isAudiencePosture(raw) ? raw : 'invite_only'with the guards disabled): markers 3/3, deleted form 0/0 →Tests 2 failed | 10 passed (12), e.g.AssertionError: expected 'invite_only' to be 'open'on the off-vocabulary pin andexpected { posture: 'invite_only' } to be undefinedon the sibling-without-posture pin.git status --porcelainempty (disk == index == HEAD). Mutated subject is same-packagesrc/resolved by vitest, so nodist/staleness in either leg.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, derivation stderr naming this tree ateb316a452): all 15 matched families green —check:auth-mount-ledger,check:route-envelope,check:slot-lookup(baseline key set verified against 071ec4e: no files added),check:test-source-alias,check:type-source-resolution,check:published-files,check:changeset-gate-self-tests,check:objectui-changeset,check-adr-0087-registration(this PR adds no declared-breaking changeset),check-changeset-no-major,check-empty-changeset,check-plugin-teardown-shape,release-rehearsal-clone --self-test, and both docs-audit gates exit 0.check:type-check-debt—32 ledger entr(ies) re-measured in 394.0s, 1843 raw tsc error(s) total, none above its recorded number(plugin-auth's zero-margin 97 holds with the new test file);check:type-check-coverage,check:engine-double-contract,check:cross-package-test-inputs(OK: 16 package(s) read outside themselves, all declared),check:where-matcher,check:query-options-erasure— all exit 0.check-nul-bytes—OK (scanned 6804 text file(s)).visibleis declaredExpressionInputSchema(CEL) but evaluated by a non-CEL grammar — a CEL predicate there silently skips the save-timerequiredgate #7169 visibility-corpus tripwire moved deliberately 94 → 96 (two new posture-gated predicates), and the gated locale bundles (zh-CN / ja-JP / es-ES; en for parity) cover the new group and keys.Changeset
minorfor@objectstack/service-settings+@objectstack/plugin-auth: three new authorable settings keys are what a host sees — a new console capability, no existing key or behaviour changes shape, nothing breaking (nevermajorin the launch window).Scope
Zero
packages/specchanges (this lane's red line — the posture's spec shape landed in #11767 and is consumed as published).plugin-security/**,plugin-sharing/**,service-messaging/**untouched.Generated by Claude Code
Generated by Claude Code