Skip to content

feat(spec,auth): guard that every feature-gated capability is UI-gated (#2874) - #2965

Merged
os-zhuang merged 8 commits into
mainfrom
claude/feature-gated-ui-guard-6kdqnw
Jul 15, 2026
Merged

feat(spec,auth): guard that every feature-gated capability is UI-gated (#2874)#2965
os-zhuang merged 8 commits into
mainfrom
claude/feature-gated-ui-guard-6kdqnw

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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== truedefault-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 即红。
  • 语义校验测试当场抓到一个 issue 清单错误:oidcProvider 实际默认开(跟随默认开的 MCP surface,resolveOidcProviderEnabled),issue 表格写的 false 已过时——注册表按 default-on(!= false)登记。

P1 — 声明式 requiresFeature 语法糖(objectui 零改动)

  • ActionSchema / ActionParamSchema 新增 requiresFeature: '<flag>'(z.enum 注册表键,拼错在解析期报错),refine 后挂 .transform()spec 解析期 lower 成规范 visible CEL(语义查注册表),与显式 visible(existing) && gate 组合,糖键从输出剥离(克隆 ADR-0089 normalizeVisibleWhen 模式);非 CEL / 纯 AST 的 visible 响亮拒绝(ADR-0078)。
  • 渲染端只见规范 visible 信封——objectui 的 filterVisibleParams/usePredicateScope零改动即生效(已核实其为通用 CEL 求值器)。FieldSchema 本次不加(现存 0 处 field 级 features 门,同一 helper 日后可扩展到 visibleWhen)。

P2 — 双面审计 + 完整性守卫

CRUD 面:

  • 既有 22 处手写 features.* 门全部迁移为 requiresFeature(含 transfer_ownership 复合门:残余行谓词留手写,门由 lowering 组合)。platform-objects.test.ts 的行为等价矩阵把每条 lowered visible.source 锁到与旧手写串逐字相同(复合行仅多括号,操作数与顺序不变)。
  • 审计补门 17 处(⚠️ 这是本 PR 有意的可见行为变化——插件关闭时这些按钮从「渲染但点了 404」变为隐藏,正是 issue 要修的诚实宣传):
    • 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 刻意不 gate sys_sso_provider 注册动作(served 值已细化为「可用」,gate 会死锁首个 IdP 引导)——理由记录在注册表。

验证

  • @objectstack/spec 6817 tests ✅、@objectstack/platform-objects 197 ✅、@objectstack/plugin-auth 440 ✅;spec 全部 69 个依赖包 turbo build ✅;改动文件 eslint ✅。
  • 三个红队 scratch 检查(未分类 flag → 漂移红;删谓词 → 正向守卫红;未登记门 → 反向守卫红)均验证后还原。
  • 端到端:--fresh 起 showcase 后端,GET /api/v1/auth/config 实测 13 布尔键与注册表一致(且 oidcProvider: true 印证默认开重分类)。
  • changeset:@objectstack/spec + @objectstack/platform-objects patch(仿 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

claude added 5 commits July 15, 2026 13:06
… + 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
@vercel

vercelBot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 15, 2026 3:38pm

Request Review

@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests protocol:ui tooling labels Jul 15, 2026
@github-actions

github-actionsBot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/spec.

100 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/plugin-auth)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/plugin-auth)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/sso.mdx(via @objectstack/plugin-auth)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/platform-objects, @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits July 15, 2026 13:40
…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
os-zhuang marked this pull request as ready for review July 15, 2026 15:05
@os-zhuang
os-zhuang merged commit e7d5291 into mainJul 15, 2026
16 of 17 checks passed
@os-zhuang
os-zhuang deleted the claude/feature-gated-ui-guard-6kdqnw branch July 15, 2026 15:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uisize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(spec,auth): guard that every feature-gated capability is UI-gated (generalize the create-user phone fix)

2 participants

@os-zhuang@claude