Skip to content

feat(spec,plugin-auth): declare plugins.admin tri-state; refuse explicit admin:false beside effective SCIM at construction (ADR-0071 conflict) - #14246

Merged
os-support-ai merged 5 commits into
mainfrom
claude/issue-13816-scim-admin-tristate-conflict
Sep 1, 2026
Merged

os-support-ai merged 5 commits into
mainfrom
claude/issue-13816-scim-admin-tristate-conflict

Conversation

@claude

@claude claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #13816

Executes the 2026-09-01 director-batch-#21 ruling (issue comment 5491056008): Shape 2, immediate — declare plugins.admin tri-state on the auth-config surface, and refuse loudly at construction time when SCIM is effective while admin is explicitly false. The measurement half (better-auth action-granularity + upgrade evaluation) is #14150 and is deliberately not touched here; #14150 remains open.

Clause-②: yes — this PR changes declared-surface parse output (a .default(false) removal) and adds a new construction-time refusal behavior.

What changed

1. Declaration — packages/spec/src/system/auth-config.zod.ts

AuthPluginConfigSchema.admin moves from z.boolean().default(false) to tri-state z.boolean().optional(), the same shape #13439 → PR #14066 gave scim / sso / ssoDomainVerification in the same file. Docblock and .describe() now state the tri-state semantics: unset ⇒ effective SCIM decides (ADR-0071 forces the admin plugin on, because SCIM's active:false deprovisioning runs through admin ban/unban); explicit true ⇒ mounted unconditionally; explicit false ⇒ declined — and beside effective SCIM, a construction-time conflict. The scim docblock's ADR-0071-coupling sentence was amended to match (its #14066 precedence prose is untouched — see boundary flags).

2. Conflict refusal — packages/plugins/plugin-auth/src/auth-manager.ts

  • New resolveScimEnabled(pluginConfig) — the single decision point for "is SCIM effective" (explicit config wins over OS_SCIM_ENABLED, per [finding] plugins.scim / plugins.sso are undeclared config keys that read as configuration but cannot ever change the outcome — the env var always outranks them #13439), now shared by the plugin mount (buildPluginList), the advertised /auth/config features flag, and the new assert, so the three can never disagree.
  • New assertScimAdminCoherence(pluginConfig) — throws a documented conflict when pluginConfig.admin === false and SCIM is effective. The message names both keys, the ADR-0071 coupling, and the two ways out (accept the admin plugin, or disable SCIM). Plain Error, no wire code — same class as the existing boot-entry asserts (assertAudienceConfig, assertOtpCooldownSeconds); it never reaches an HTTP envelope, so no ADR-0112 ledger entry is due (check:dispatcher-error-vocabulary green confirms).
  • Called at three entry points: the AuthManager constructor (boot refusal, the established pattern), applyConfigPatch on the merged result (no smuggling path; the standing config keeps ruling), and buildPluginList (where the env var is actually read — an OS_SCIM_ENABLED appearing between construction and the lazy better-auth build is still refused).
  • The three ?? scimEffective fallbacks are kept: absent admin under effective SCIM still gets the admin plugin forced on (ADR-0071-backed current behavior); this PR only adds the loud exit for the explicit decline.

3. Generated artifacts + ledger

  • packages/spec/scripts/lib/default-changes.ts: the default move system/AuthPluginConfig:admin false → (none) is declared under major 17 with a full reason (the spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666 ratchet accepted it; the build prints the reason).
  • packages/spec/authorable-defaults/system.json (row removed) and content/docs/references/system/auth-config.mdx regenerated by check:generated --fix; everything else reproduced byte-identical.
  • Changeset: @objectstack/spec minor + @objectstack/plugin-auth minor (launch-window MINOR rule; a declared-surface change plus a new refusal behavior, no removal/rename — ADR-0087 marker: not-required, no-migration-prescription, since a conflicted config needs a human decision between accepting admin and disabling SCIM, nothing mechanical to rewrite).

Derived judgments (Clause-②, itemised)

  1. .default(false) removal — census of every parse consumer of admin. Runtime parse consumers of AuthConfigSchema/AuthPluginConfigSchema: none (grep census; the only non-spec reference is a comment in auth-manager.ts — the runtime receives plain objects, exactly as the audience docblock records). The two live read points of plugins.admin are the ?? chains in auth-manager.ts (buildPluginList and the /auth/config features mirror), both fed raw config, both verified byte-identical in absent-key behavior. Gates on messages naming auth.plugins.admin (admin-import-users, auth-plugin 501 paths) gate on the mounted API (typeof authApi.createUser), not on the config value — unaffected. AuthPluginConfigParsed has zero external consumers (declaration-only). The z.input type is unchanged (admin was already optional on the input side); only z.infer moves from boolean to boolean | undefined, and spec typecheck plus the full spec suite (448 files / 12035 tests) pass with no expectation on the materialized default anywhere.
  2. Tri-state semantics. Absent / true / false are now three distinct authored states; a parse no longer materialises false onto a silent document. Before this change, any authoring path that parsed through the spec schema would hand the runtime an explicit-looking false that silently defeated the ADR-0071 forced coupling; the tri-state makes parse output faithful to the runtime's raw-config contract.
  3. New refusal's trigger surface. Exactly one corner refuses: admin === false AND effective SCIM. Coherent shapes proven accepted by tests: scim: false, admin: false beside an ambient OS_SCIM_ENABLED (explicit config wins — declining both is legal); admin: false with SCIM off everywhere; admin: true anywhere; absent admin under SCIM (forced on, unchanged). Trigger entry points: constructor, config patch (merged result), lazy build (late env). Deployment-visible consequence, named: a deployment already running the contradiction booted with silently broken SCIM deprovisioning (every active:false ban attempt failing at runtime); it now refuses to construct, with the remedy in the message — that loud stop is the ruled behavior.

Verification

Union runs at the final merged head 9238222d3 (all commands re-run there after the last git merge origin/main):

  • pnpm --filter @objectstack/plugin-auth test — 87 files / 1803 passed (includes 7 new pins: 4 refusal + 3 acceptance).
  • pnpm --filter @objectstack/spec test — 449 files / 12052 passed.
  • pnpm --filter @objectstack/plugin-auth typecheck and pnpm --filter @objectstack/spec typecheck — both exit 0.
  • Ablation (reverse verification): with assertScimAdminCoherence neutralized (mutation proved on disk, marker grep 0 → 1), exactly the 4 refusal tests go red (4 failed / 263 passed in auth-manager.test.ts) and the acceptance tests stay green; restore proven by blob identity against HEAD (git hash-object equals the HEAD blob, git diff HEAD empty). The test imports in-package src, so no dist-resolution leg applies.
  • Full derived gate family (scripts/pm/dispatch-gates.mjs, no-paths derivation — re-derived at the merged head, list unchanged) run green locally; check:test-completeness is NOT MEASURED locally by its own contract (it grades a saved CI test log; left to CI). check:generated reproduces all 15 artifacts byte-identical on the merged tree. check:type-check-debt re-measured on the merged tree — no entry above its recorded number (ledger untouched).
  • Known measurement gap, stated: plugin-auth's tsconfig.json excludes test files from typecheck and carries no sibling test-typecheck config, so the edited test file's type health is exercised by vitest only — the pre-existing repo-wide gap already tracked by [finding] No plugin package has a tsc program that compiles its tests — plugin-approvals alone hides 345 type errors, and 0 of 14 are in the test-typecheck ledger #14062 (no new filing).

Boundary flags (not re-judged here)

Generated by Claude Code


Generated by Claude Code

…cit admin:false beside effective SCIM (ADR-0071 conflict)

Part of #13816 — WIP before artifact regeneration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…in tri-state; declare the default move; add changeset

Part of #13816.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…doc-authoring gate); keep it in comments

Part of #13816.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actions github-actions Bot added the size/m label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-defaults/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/kernel/services-checklist.mdx (via OS_SCIM_ENABLED (literal, a string literal in buildPluginList; a string literal in getPublicConfig; a string literal in resolveScimEnabled))
  • content/docs/permissions/sso.mdx (via getPublicConfig (symbol, a method of class AuthManager))

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

  • content/docs/releases/implementation-status.mdx (via OS_SCIM_ENABLED (literal, a string literal in buildPluginList; a string literal in getPublicConfig; a string literal in resolveScimEnabled))

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
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-defaults/system.json) — 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 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 b360cc7d5ca2e9cdb60a12018af20cbc3470cafa → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0890a6356b60fef6264cd255676fb8db1860ca0a — the merge of head 9238222d3b638497b8f1990964025b9691c3ebdf into base b360cc7d5ca2e9cdb60a12018af20cbc3470cafa, 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 0890a6356b60fef6264cd255676fb8db1860ca0a && git checkout 0890a6356b60fef6264cd255676fb8db1860ca0a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b360cc7d5ca2e9cdb60a12018af20cbc3470cafa 9238222d3b638497b8f1990964025b9691c3ebdf && git checkout -B drift-repro b360cc7d5ca2e9cdb60a12018af20cbc3470cafa && git merge --no-ff 9238222d3b638497b8f1990964025b9691c3ebdf

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

⚠️ 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 b360cc7d5ca2e9cdb60a12018af20cbc3470cafa → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 1, 2026
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 16:07
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit a7e18de Sep 1, 2026
41 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13816-scim-admin-tristate-conflict branch September 1, 2026 16:49
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 protocol:system size/m tests tooling

Projects

None yet

2 participants