Repository navigation
[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
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Aug 31, 2026 Triage →
domain:spec·needs-user-decision·pm:blocking· p1.Anchoring. Option A's declaration half lands in
packages/spec/src/system/auth-config.zod.ts; per the lane rule, anything touchingpackages/specroutes to the spec seat as sole owner regardless of who needs it, so theplugin-authprecedence half rides along under that seat.pm:blockingbecause objectstack-ai/cloud#1451 is parked on this ruling.Re-verified on current
main(the card measured at cloud's pin; both halves still hold):packages/plugins/plugin-auth/src/auth-manager.ts :2426 const scimEffective = scimFromEnv ?? (pluginConfig as any).scim ?? false; :2439 admin: pluginConfig.admin ?? scimEffective, :2445 scim: scimEffective, :5064 admin: pluginConfig.admin ?? (readBooleanEnv('OS_SCIM_ENABLED') ?? (pluginConfig as any).scim ?? false),The
adminamplification is the sharper half and I want it named in the ruling, not left as a rider.
四维分析
① 实际业务需求. 不是假想需求 —— 已经绕过一次了。cloud 的 tenant-kernel factory 必须在
OS_SCIM_ENABLED存在时拒绝构建(cloud#1265),正因为它算出的plugins.scim压不过环境变量;cloud#1451 现在停在这个决定上。已经有一个下游为这个缺口付过代价。② 项目长远合理性. 「声明即强制」是本仓已有的原则,
dynamicClientRegistration已经是三态z.boolean().optional()的样板。A 不是发明新机制,是把已有形状补齐到这三个键。反过来看现状:schema 不声明、代码用any读、且永远压不过环境变量 —— 三重不诚实叠在同一个键上。③ 防 AI 写代码犯错(本案最重的一轴). cloud 侧原本的修复草案就是「在控制面构造 AuthPlugin 时显式传
scim: false」——一行看起来像安全控制、过 review、过 typecheck、且什么都不做的代码。这正是 agent 最容易写、reviewer 最难否掉的那一类。A 之外的任何选项都把这个陷阱原样留着。④ 创业阶段不扩散需求. A 的改动面有界:三个键的声明 + 一段优先级顺序,且只在 host 显式给值时才改变行为,任何不设这三个键的部署零影响。B 要新造一条 host-forced 通道(新概念、新文档、新测试面),是扩散。C 最便宜,但把误导性 affordance 原样留着 —— 等于花钱买一个「已记录」的标签。
建议 A,并请一并裁一个本卡未拆开的问题:
admin: pluginConfig.admin ?? scimEffective意味着开了 SCIM 就自动获得 better-auth 的admin插件 ——impersonate-user/set-user-password/ban-user—— 且同样没有拒绝的手段。这比三个键本身是更大的权限面问题。它是随 A 同批裁决,还是单独立卡? 若同批,A 的范围就不止「三个键」,PR 面会明显变大;若单独,本卡落地后那条放大仍然存在。⚠️ Clause-② 适用于实现卡:路径肢packages/spec/src/**,内容肢 = 新增声明键即扩宽公共 accept 面。实现卡须按CONTRACT_REVIEW_TIER派发并申报Clause-②: yes。
Generated by Claude Code
huangyiirene commented
on Aug 31, 2026 CollaboratorMore actions裁决:A —— 三键入 schema、显式配置压过 env;admin 放大项拆卡 #13816(维护者 2026-08-31)
项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· 决裁批 #19 ①维护者原话(逐字):「批 #19 同意」—— 本卡呈报推荐为「A,admin 放大项拆单独卡」。
裁决内容
scim/sso/ssoDomainVerification三键补进AuthPluginConfigSchema(packages/spec/src/system/auth-config.zod.ts),三态z.boolean().optional(),照dynamicClientRegistration的既有样板,docstring 写明 unset 语义;- 优先级翻转:显式配置 > env;env 只在配置未设处决定(操作员按环境覆盖的能力保留于 unset 情形)。落点
packages/plugins/plugin-auth/src/auth-manager.ts的??链(:2426/:2446及同族读点); - 范围纪律照卡面:仅当 host 显式给值时行为才变,不设这三键的部署零影响;
- admin 放大项(
:2439admin: pluginConfig.admin ?? scimEffective,镜像:5064)⛔ 不入本卡范围 —— 已拆 [finding] SCIM forces the better-authadminplugin on (pluginConfig.admin ?? scimEffective) and a deployment has no declared way to decline it — split from #13439 by the 2026-08-31 ruling #13816。拆的依据:该耦合是 ADR-0071 记录在案的刻意行为(SCIMactive:false→ 封禁走 admin 插件),动它要重审 ADR,不是本 p1 的 rider。
落地要求
- 条款②:YES(路径肢
packages/spec/src/**+ 内容肢:新增声明键扩宽 accept 面、已文档化优先级变化),按契约复审档位派发,PR 自报Clause-②: yes; - changeset 标行为变化;已知风险具名进 PR:同时写显式值与 env 且依赖 env 赢的部署会翻面(已知显式写入方仅 cloud,且 cloud 正盼翻面 —— A 落地后 cloud#1265 的拒绝构建 workaround 应可退役,cloud#1451 解锁)。
状态转移(同笔)
needs-user-decision→pm:queue;bug/priority:p1/domain:spec/pm:blocking不动(pm:blocking待实现落地随卡关摘除 —— cloud#1451 停在实现上,不再停在裁决上)。
Generated by Claude Code
os-support-ai commented
on Sep 1, 2026 CollaboratorMore actionsClaim: PM loop round R1
Session:session_01Mciyv38maJ6HYVMiaM26T1
Branch:claude/issue-13439-auth-config-declared-scim-sso
Worktree:objectstack-issue-13439
Domain:domain:spec
File surface:packages/spec/src/system/auth-config.zod.ts(three new tri-state keys) +packages/plugins/plugin-auth/src/auth-manager.ts(?? precedence chains at :2426/:2443/:2444/:5109 and same-family read points; theadmincoupling at :2439/:5064-family is EXCLUDED — split to #13816) + tests + changeset + regenerated spec artifacts (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: claude-fable-5— dispatch-gates --tier @9b18c04: "no path-derived mandate … Clause ② SUSPECT surface: packages/spec/src/system/auth-config.zod.ts ⇢ 'packages/spec/src/**' — the normal landing zone of a clause-② card"; content limb ruled YES by the 2026-08-31 maintainer ruling (new declared keys widen the accept set + documented precedence change) ⇒ CONTRACT_REVIEW_TIER mandatory
Clause-②: yes
Serial constraints cleared: no open PR or in-flight claim touchesauth-manager.tsorauth-config.zod.ts(open-PR scan 2026-09-01T00:2xZ, 21 PRs; services lane in-flight 0 per seat post #6021); cross-domain single-PR path — triage comment 5478740503 routes the plugin-auth half under this seat; predecessor's linger PR #13794 surface is disjoint (file-description.ts + 10 other zod files); sibling decision card #13816 (admin amplification) is openneeds-user-decision— its scope is excluded here, and this card pins none of its behavior.
Generated by Claude Code
Dev claim (dispatched): session
session_01Mciyv38maJ6HYVMiaM26T1
Branch:claude/issue-13439-auth-config-declared-scim-sso(pushed, empty, write-route probe green)
Worktree:objectstack-issue-13439@ e612dd0 (origin/main at claim time; PM verified @9b18c04 — will re-verify every quoted line against this newer main)
Scope per maintainer ruling comment 5479233268: three tri-state keys into AuthPluginConfigSchema + precedence flip (explicit config wins over env) in auth-manager.ts read points; admin coupling EXCLUDED (split to #13816). Clause-2: yes — will hangneeds:contract-reviewon PR and card at PR creation.
Generated by Claude Code
os-dev-report
{ "issue": 13439, "status": "done", "branch": "claude/issue-13439-auth-config-declared-scim-sso", "pr": "https://github.com/objectstack-ai/objectstack/pull/14066", "premise_still_valid": true, "summary": "Both halves of the ruling implemented at head 67ab1187 (origin/main merged in). (1) AuthPluginConfigSchema declares scim/sso/ssoDomainVerification as tri-state z.boolean().optional() per the dynamicClientRegistration template, docstrings state unset semantics and the flipped precedence. (2) All six behavior read points in auth-manager.ts flipped to explicit-config-wins (buildPluginList x3, the /auth/config admin-default inline recomputation, isSsoWired, isSsoDomainVerificationEnabled); the as-any casts on the three keys removed; the stale isSsoWired docstring documenting old precedence rewritten. ADR-0071 admin coupling shape untouched per ruling clause 4; the flipped scim resolution flows through it (measured, named in PR body; no conflict with ADR-0071 - it pins the coupling, not scim resolution order). needs:contract-review hung on PR 14066 and card 13439 at PR creation, additive endpoint, read back on both. Clause-2: yes self-reported in PR body. Premise re-verified at e612dd0 before editing: keys undeclared, chains env-first, all quoted snippets held.", "tests": "All at final head 67ab1187 (union re-run after the origin/main merge): plugin-auth suite 'Test Files 87 passed / Tests 1796 passed'; spec suite 'Test Files 447 passed / Tests 11991 passed'; both typechecks exit 0; spec check:generated 'All 15 generated artifacts are up to date'. New pins: full matrix per key (explicit false + env truthy stays false incl. empty-string env; explicit true + env falsy stays true; unset follows env; unset + absent off; features.admin mirror). Reverse verification from committed state, direction red as predicted: env-first revert of isSsoWired turned exactly the 9 explicit-config-wins pins red, restore proven by empty git diff HEAD + blob-hash identity (be7f389f), rerun green. Mutation legs confirmed on disk by anchor-string counts (1 old-form / 0 new-form post-mutation); no rebuild needed for the ablation - same-package relative src import, vitest transforms src (spec dist WAS rebuilt before all measurement). Reverse type check: bogus key red with TS2339 listing the three new keys, proving the rebuilt spec d.ts is what tsc reads. check:type-check-debt full re-measure green: '28 ledger entries re-measured, none above its recorded number'; scoped probe reproduced plugin-auth ledger exactly (94 total / 43 in auth-manager.test.ts) = zero new test-typecheck errors.", "mcp_calls": "0 - all reads via git + public-page payload JSON + repo-scoped REST (probe 200 at session start); all writes (claim comment, PR create, labels, this comment) via REST additive endpoints with read-backs", "gates": "Derived at 67ab1187 via dispatch-gates.mjs (no paths; stderr named this repo+commit). Green with own judgment lines: check:generated family (docs/authorable-surface/api-surface/liveness/strictness-ledger etc, 15/15), check:changeset-gate-self-tests, check:merge-driver, check:page-declaration-shape, check:slot-lookup, check:spec-parsed-alias, check:logger-receiver-detach, check:cross-package-test-inputs, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:dispatcher-error-vocabulary, check:objectql-double-limit, docs family (doc-authoring/doc-anchors/docs-single-h1/docs-redirects/docs-audit-scope/role-word/corpus-claim-drift/quick-reference-counts/doc-frontmatter/doc-route-spelling/docs-section-name/section-landing-index), check:published-files, check:published-readme-links, check:skill-identifier-liveness, check:react-page-adapter-contract, check:test-source-alias, check:type-source-resolution, check:pm-half-states, check:objectui-changeset, spec llms-txt/empty-state/skill-examples/yaml-examples/variant-docs, lint doc-formula-expressions/doc-security-posture, check:nul-bytes, check:dual-build-cjs-loads (after full workspace build), check:type-check-coverage + check:type-check-debt, check-adr-0087-registration ('no declared-breaking changeset'), check-changeset-no-major, check-empty-changeset, check-ci-filter-parity, check-comment-mask-adoption, check-dev-prereqs, check-keyed-text-bounds, check-plugin-teardown-shape, check-tenant-audit-census, check-undeclared-dep-imports, release-rehearsal-clone --self-test, docs-audit/check-affected-docs.", "files_changed": [ "packages/spec/src/system/auth-config.zod.ts (+49)", "packages/plugins/plugin-auth/src/auth-manager.ts (+36/-17 incl. comment updates)", "packages/plugins/plugin-auth/src/auth-manager.test.ts (+172/-1)", "content/docs/references/system/auth-config.mdx (+6, generated)", "packages/spec/authorable-surface/system.json (+3, generated)", ".changeset/declared-scim-sso-explicit-config-wins.md (+43, minor spec + minor plugin-auth, behavior change + risk named, adr-0087 not-required marker)" ], "line_budget": "no skills/** files in diff - no line-budget readings owed", "deviations": [ "Declared narrowing: qa/dogfood suites not run locally - the four files setting OS_SCIM_ENABLED do so with no explicit plugins key (their own comments: bootStack exposes no auth-plugin override), i.e. entirely in the unchanged unset-config quadrant; CI runs them regardless", "CI-context gates left to CI: check-shard-attestation, check-test-completeness, docs-audit drift comment (need CI/PR runtime context)", "check:llms-txt first attempt was a root-level spelling miss (exit 254 'Did you mean') - read as NOT MEASURED, re-run correctly spec-filtered, green", "First full-build lock call cap-killed at 18/19 tasks (exit 143); no measurement taken from it; retried to 26/26 and 70/70 green via turbo cache", "Terminal-report format: dispatch prompt asked machine-checkable fields only (gates/line_budget/deviations/files_changed); os-dev standing contract requires the fuller JSON template - per the conflict rule the standing template wins, PM fields merged in as extra keys", "PR body read-back: prose survived byte-identical; platform appended its own bare footer after a horizontal rule (known platform append, another actor's write, left alone)" ], "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
os-support-ai commented
on Sep 1, 2026 CollaboratorMore actionsACCEPT — PR #14066 (head
67ab1187), reviewer of record:domain:specseat,session_01Mciyv38maJ6HYVMiaM26T1.Checklist conclusions (verified against the full diff, read hunk-by-hunk, not the report's narration):
- Form: draft ✓ · base
main✓ ·Fixes #13439first line ✓ · 6 files +287/−14, exactly the claimed surface (schema + auth-manager + its test + two generated artifacts + changeset) ✓ - Ruling compliance, all four clauses: ① three keys declared tri-state
z.boolean().optional()per thedynamicClientRegistrationtemplate, docstrings state unset semantics AND the deliberate opposite-of-env-wins precedence ✓ ② all six family read points flipped (buildPluginList ×3, the/auth/configadmin-default inline recomputation,isSsoWired,isSsoDomainVerificationEnabled),as anycasts removed, the stale env-wins docstring rewritten ✓ ③ zero-change-when-unset pinned by the retitled unset-config tests; the OIDC/2FA/HIBP keys keep env-wins, documented in the new comment block ✓ ④ ADR-0071 admin coupling SHAPE untouched (pluginConfig.admin ?? scimEffectiveunchanged; only the inner scim resolution flipped), the flow-through consequence named in PR + changeset and PINNED (explicitscim:false+ env truthy ⇒ neither scim nor admin registered,features.adminmirror agrees) — measured no conflict with the ADR, which pins the coupling, not the resolution order ✓ - Spot-checks: reverse verification (env-first revert turned exactly the 9 explicit-config-wins pins red, restore blob-proven) ✓ · empty-string-env case pinned (a present value under
readBooleanEnvloses to explicit config) ✓ · docs-drift advisory cleared by this seat's own reading — the four flagged pages state no scim/sso precedence (authentication.mdx's "Env wins" lines are aboutOS_AUTH_SIGNUP_ENABLED/OS_AUTH_GOOGLE_ENABLED, checklist's "gated by" stays true for the unset case) ✓
Clause-② in-seat contract review: PASS (fuse read this session:
get_session.external_metadata.last_served_model = claude-fable-5=CONTRACT_REVIEW_TIER; provenance: 2026-08-31 in-seat review ruling).- Derived judgments: accept-set widening = exactly the three ruled keys (authorable-surface/system.json +3 rows, generated;
check:generated15/15, no other surface movement); the precedence flip is the ruled, documented behavior change scoped to explicit-value deployments (known writer: cloud, which requires it — cloud#1265 workaround retires, cloud#1451 unblocks on landing). - Semver: minor for both
@objectstack/spec(widening) and@objectstack/plugin-auth(behavior change) — correct; ADR-0087 disposition markernot-requiredjustified (nothing removed/renamed/re-shaped, no mechanical rewrite formigrate meta). - Boundary flags: no open questions; deviations reviewed — the qa/dogfood local narrowing is sound (those suites sit in the unchanged unset-config quadrant, and CI's Dogfood Regression Gate runs them regardless); the terminal-report format conflict was resolved correctly (standing os-dev contract wins, PM fields merged).
Clearing
needs:contract-reviewon both carriers this stroke. Landing: governed predicate 0/6 (register-derived, final file list) ⇒ ordinary queue path; flip checkpoint armed — ready + auto-merge on green CI at67ab1187.pm:blockingstays until the card closes (cloud#1451 waits on the LANDING, per the ruling's state note).
Generated by Claude Code
- Form: draft ✓ · base
- added a commit that references this issue
on Sep 1, 2026
Surfaced from the cloud side while implementing objectstack-ai/cloud#1451 (hardening the cloud control plane's own AuthPlugin against an ambient
OS_SCIM_ENABLED). Filed unassigned, recording a framework contract gap; the cloud card is parked on the decision this issue describes.Measured at
1a540e82b1421c26ea036ff67e381689075b501c(the SHAcloud/.objectstack-shacurrently pins) and re-checked onmainat the same line — no drift between them.The two halves
1. The keys are not declared.
AuthPluginConfigSchema(packages/spec/src/system/auth-config.zod.ts:20) declares exactly ten keys:organization,twoFactor,passkeys,passwordRejectBreached,magicLink,oidcProvider,dynamicClientRegistration,deviceAuthorization,admin,phoneNumber.buildPluginListnevertheless reads three more off the same object through ananycast (packages/plugins/plugin-auth/src/auth-manager.ts):So
plugins.scimis a key an author can write, that typechecks only because of the cast, that no schema documents, and that no publish-time check would ever reject or confirm.2. Where it IS read, the env var always wins.
readBooleanEnv(auth-manager.ts:256) returnsundefinedonly when the variable is absent; any present value — including the empty string — resolves to a boolean. BecausescimFromEnvis the left operand of the??chain, a host that writesplugins: { scim: false }gets no effect at all wheneverOS_SCIM_ENABLEDis set. The same holds forsso/OS_SSO_ENABLED.The precedence itself is deliberate and documented in the block comment above it ("the env var WINS over the config-file setting so platform operators can override per-environment without touching the application bundle"). The gap is not that operators can override — it is that a host has no counterpart: there is no declared way for the code that constructs
AuthPluginto state a value the deployment env cannot silently outrank.Why this is worth a decision rather than a patch
The combination is the sharp part. An undeclared key that is read anyway reads, to any author, as the supported way to say "not on this surface" — and it is silently inert in exactly the deployments where it matters. The cloud-side card walked into precisely that: its accepted fix sketch was "pass an explicit
scim: falseat the control plane's AuthPlugin construction", which the reading above shows would have shipped a line that looks like a security control, passes review, passes typecheck, and changes nothing.dynamicClientRegistrationalready shows the shape the platform uses when a host needs to force a value against a default: a declared, tri-statez.boolean().optional()whose docstring spells out what unset means.scimandssohave neither the declaration nor the tri-state.Amplification worth naming, since it is not local to the SCIM line:
admin: pluginConfig.admin ?? scimEffective(:2439, mirrored at:5064). A deployment that never asked for the better-authadminplugin gets it —impersonate-user,set-user-password,ban-user— as a side effect of the same variable, and has the same non-existent means of declining it.Options
scim,sso,ssoDomainVerificationinAuthPluginConfigSchemaas tri-statez.boolean().optional(), and give an explicit config value precedence over the env (env keeps deciding only where config leaves it unset). Restores "declared = enforced", keeps the operator override for the unset case, and gives a host a real way to say no.Recommendation is A: it is the only option under which the key means what an author reading it would assume, and it is the direction the platform already took for
dynamicClientRegistration. It also removes the root cause of a workaround already shipped downstream — cloud's tenant-kernel factory has to refuse to build whenOS_SCIM_ENABLEDis present precisely because the plan-derivedplugins.scimit computes cannot outrank the variable (objectstack-ai/cloud#1265). Under A that plan gate would simply be authoritative.Note on scope discipline: A changes documented precedence for these three keys only, and only for the case where a host states a value explicitly — no deployment that leaves them unset sees any change.
Not reproduced against a running deployment; this is read off the source at the SHA above plus the corroborating comments in cloud's own
artifact-kernel-factory.scim.test.ts/.scim-env-guard.test.ts, which independently document the same resolution order.