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

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 15, 2026 3:38pm

Request Review

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

github-actions Bot 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 main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

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