feat(spec,auth): guard that every feature-gated capability is UI-gated (#2874)#2965
Merged
Conversation
… + drift guard (#2874 P0) Single registry (PUBLIC_AUTH_FEATURES) classifying all 13 boolean flags served by getPublicConfig() at /auth/config: consumption surface (crud/login/status), default semantics (opt-in '== true' vs default-on '!= false'), and gated spec inputs or an exemption reason. Includes featureGatePredicate + lowerRequiresFeature helpers for the upcoming requiresFeature sugar (P1). The plugin-auth drift guard derives the ACTUAL served key set from a real AuthManager and asserts it equals the registry keys, so a new flag that ships unclassified turns CI red. A semantics check also asserts each flag's registered default matches what getPublicConfig() actually serves - it already caught oidcProvider being default-ON (follows the MCP surface), which issue #2874's own table misclassified as opt-in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
…red to visible CEL (#2874 P1) ActionSchema and ActionParamSchema gain an optional requiresFeature: <flag> (z.enum over the registry keys, so a typo fails the parse). A .transform() appended after the schemas' refinements lowers it at parse time into the canonical visible predicate - features.X == true for opt-in flags, features.X != false for default-on (per PUBLIC_AUTH_FEATURES semantics) - AND-composing with any explicit visible ((existing) && gate) and stripping the sugar key from the output, mirroring the normalizeVisibleWhen pattern (ADR-0089). A non-CEL or AST-only visible rejects loudly (ADR-0078) instead of silently dropping the gate. Renderers, lint, and objectui only ever see the canonical visible envelope - the objectui rendering chain needs zero changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
…tes to requiresFeature (#2874 P2a) Mechanical migration of every visible: 'features.X ...' predicate in the identity objects to the declarative requiresFeature annotation, including the compound transfer_ownership gate (residual row predicate stays hand-written; the lowering AND-composes the feature gate onto it). A behavior-equivalence matrix in platform-objects.test.ts pins each lowered visible.source to the exact CEL string that was previously hand-written (parenthesized-composition for the compound row), and asserts the sugar key never survives into parsed objects - so the migration is provably behavior-neutral. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
…vider surfaces + bidirectional completeness guard (#2874 P2b) Audit fixes - capability-dependent actions that previously rendered with the backing plugin off and then 404'd: - requiresFeature: 'admin' on the six sys_user platform-admin actions (ban/unban/unlock/set_user_password/set_user_role/impersonate_user); the stale 'still render but server returns 404' comment is updated. SCIM deployments keep them (SCIM forces features.admin on, ADR-0071). - requiresFeature: 'twoFactor' on the three sys_two_factor actions and the three sys_user self-service 2FA actions (composed onto their existing record.id == ctx.user.id predicates). - requiresFeature: 'oidcProvider' on the five sys_oauth_application actions (composed onto the enable/disable record predicates). feature-gate-guard.test.ts asserts registry<->objects lockstep in both directions: every gatedInputs path resolves to a real input carrying the flag's lowered predicate (removing an annotation goes red), and every features.* reference in a visible predicate is booked in the registry (adding a gate without bookkeeping goes red). Login-surface audit gaps are recorded in the registry and tracked in objectui#2513 (DeviceAuthPage unguarded) and objectui#2514 (passkeys/magicLink advertised but unconsumed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
…#2874) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 100 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…params (#2874) Also lists the param-level visible predicate (added in #2871) in the ActionParam field summary, which had drifted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
…d reserved word in docs example - Spec property liveness ratchet: requiresFeature is live at parse time (lowered into the already-live visible predicate and stripped), with evidence pointing at lowerRequiresFeature and the #2874 guard tests. - check-role-word (ADR-0090 D3): swap the docs composition example from transfer_ownership to disable_oauth_application so no new use of the reserved word ships. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
…nerated action reference doc - api-surface: the #2874 registry exports (PUBLIC_AUTH_FEATURES et al.) are intentional public API - snapshot updated via gen:api-surface. - references/ui/action.mdx: regenerated. ActionParamSchema now ends in a .transform() (the requiresFeature lowering), which z.toJSONSchema cannot represent on the output side, so the generator drops the ActionParam block - the same pre-existing behavior as ActionSchema and ObjectSchema. The authoritative ActionParam field documentation lives in the hand-written protocol doc (content/docs/protocol/objectui/actions.mdx), updated in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
os-zhuang
marked this pull request as ready for review
July 15, 2026 15:05
68 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2874。把 create-user phone 修复(#2871 / objectui#2406)推广为机制:UI 宣传了 runtime 没有的能力,因为背后的插件没开——这类静默缺口从此由注册表 + 声明式标注 + 双向 CI 守卫兜住。按 issue v2 的三层交付:
P0 — flag 分类注册表 + 清单漂移守卫
packages/spec/src/kernel/public-auth-features.ts:PUBLIC_AUTH_FEATURES注册表,分类/api/v1/auth/config广告的全部 13 个布尔 flag——消费面(crud/login/status)、默认语义(opt-in→== true、default-on→!= false)、gated spec inputs 或豁免理由(XOR,测试强制)。模块零 import,可被 schema 文件直接引用无环。plugin-auth/src/public-feature-registry.test.ts,仿mcp/skill-surface-guard模式):从真实AuthManager.getPublicConfig()取实际键集 ≡ 注册表键集;非布尔键 ⊆ {termsUrl, privacyUrl};键集跨配置稳定。已红队验证:往 features 字面量加未分类 flag 即红。oidcProvider实际默认开(跟随默认开的 MCP surface,resolveOidcProviderEnabled),issue 表格写的 false 已过时——注册表按default-on(!= false)登记。P1 — 声明式
requiresFeature语法糖(objectui 零改动)ActionSchema/ActionParamSchema新增requiresFeature: '<flag>'(z.enum注册表键,拼错在解析期报错),refine 后挂.transform()在 spec 解析期 lower 成规范visibleCEL(语义查注册表),与显式visible以(existing) && gate组合,糖键从输出剥离(克隆 ADR-0089normalizeVisibleWhen模式);非 CEL / 纯 AST 的visible响亮拒绝(ADR-0078)。visible信封——objectui 的filterVisibleParams/usePredicateScope链零改动即生效(已核实其为通用 CEL 求值器)。FieldSchema 本次不加(现存 0 处 field 级 features 门,同一 helper 日后可扩展到visibleWhen)。P2 — 双面审计 + 完整性守卫
CRUD 面:
features.*门全部迁移为requiresFeature(含transfer_ownership复合门:残余行谓词留手写,门由 lowering 组合)。platform-objects.test.ts的行为等价矩阵把每条 loweredvisible.source锁到与旧手写串逐字相同(复合行仅多括号,操作数与顺序不变)。admin×6:sys_user的 ban/unban/unlock/set_user_password/set_user_role/impersonate_user(文件里自己的注释原本就承认「插件关了照样渲染、后端 404」,已同步更新;SCIM 部署不受影响——ADR-0071 强制features.admin开)。twoFactor×6:sys_two_factor×3 +sys_user自助 ×3(与既有record.id == ctx.user.id && ...谓词组合)。oidcProvider×5:sys_oauth_application全部动作(enable/disable 与record.disabled残余谓词组合;因默认开,vanilla 部署无变化)。platform-objects/src/feature-gate-guard.test.ts)双向:正向——注册表每条 gatedInput 路径必须解析到真实 input 且带匹配谓词(删requiresFeature即红);反向——对象里每个features.*引用必须是注册表 flag 且已登记(加门不记账即红)。双向均已红队验证。登录面(核对为主,结论落注册表):
sso/ssoEnforced/phoneNumberOtp/phoneNumber消费无缺口;twoFactor不被 LoginForm 读取属刻意设计(服务端 remediation,ADR-0069,仅注记);缺口开了 follow-up:objectui#2513(DeviceAuthPage 裸打端点不查features.deviceAuthorization)、objectui#2514(passkeys/magicLink有广告无 UI)。sso刻意不 gatesys_sso_provider注册动作(served 值已细化为「可用」,gate 会死锁首个 IdP 引导)——理由记录在注册表。验证
@objectstack/spec6817 tests ✅、@objectstack/platform-objects197 ✅、@objectstack/plugin-auth440 ✅;spec 全部 69 个依赖包turbo build✅;改动文件 eslint ✅。--fresh起 showcase 后端,GET /api/v1/auth/config实测 13 布尔键与注册表一致(且oidcProvider: true印证默认开重分类)。@objectstack/spec+@objectstack/platform-objectspatch(仿 feat(spec,platform-objects): action param 'visible' predicate; gate create-user phone on features.phoneNumber #2871 additive 先例;plugin-auth 仅测试改动不 bump)。🤖 Generated with Claude Code
https://claude.ai/code/session_01HGofzwNXvtahMGk8TQqXdx
Generated by Claude Code