Skip to content

feat(spec,security): nav landing exclusivity + field-permission predicate guard (anti filter-oracle)#2630

Merged
os-zhuang merged 2 commits into
mainfrom
claude/spec-filters-exclusivity
Jul 5, 2026
Merged

feat(spec,security): nav landing exclusivity + field-permission predicate guard (anti filter-oracle)#2630
os-zhuang merged 2 commits into
mainfrom
claude/spec-filters-exclusivity

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Completes the framework half of objectstack-ai/objectui#2251 (objectui ADR-0055) — the two guards that make AI/author mistakes either unwritable or harmless.

1. spec — ObjectNavItem landing exclusivity (correct-by-construction)

NavigationItemSchema now rejects an object nav item combining filters with recordId or viewName. Runtime precedence (recordIdfiltersviewName) would silently ignore the extras — the failure mode being a stale recordId hijacking a configured filters slice. Following ADR-0053's philosophy, the ambiguous state is now unrepresentable rather than precedence-resolved.

  • Implemented as superRefine on the union member (the base ObjectNavItemSchema stays .extend()-able); custom issue anchored at filters with the fix in the message.
  • The legacy recordId + viewName combination stays tolerated (documented: viewName is ignored when recordId is set) — zero released-metadata impact; filters itself ships in the same unreleased minor (feat(spec): ObjectNavItem.filters — declarative slices on the bare data surface #2626).
  • 4 new schema tests (accept filters-only, reject filters+viewName, reject filters+recordId, tolerate legacy combo). api-surface.json unchanged.

2. plugin-security — field-level predicate guard (anti filter-oracle)

The hole: FieldMasker strips non-readable fields from results, but nothing sanitized predicates. Filtering salary >= 100000 (or sorting/grouping/aggregating by salary) changes which rows return even though the column is masked — row presence is the oracle. objectui's /data surface (URL-driven filter[...], objectui#2255) makes arbitrary predicates first-class, so the engine must close this independent of the client.

The fix: the security middleware rejects (403 PermissionDeniedError, details.reason = 'field_predicate_denied', offending fields listed) any caller query whose where / orderBy / groupBy / having / aggregations (field + FILTER) / windowFunctions (field + partitionBy + over.orderBy) reference a field the caller's field permissions mark non-readable.

Design decisions:

  • Reject, never silently rewrite — dropping an $or branch narrows results, dropping an $and branch widens them and re-opens the oracle. Salesforce FLS errors the same way; the message carries the prescription.
  • Runs against the caller's AST, before RLS injection — RLS policies legitimately reference fields the caller can't read (e.g. owner_id) and must not be rejected. The guard's position in the middleware defines the caller/system boundary.
  • Projection (fields) is deliberately NOT guarded — selecting a hidden field is harmless because FieldMasker strips it; only predicates leak.
  • Pure logic in predicate-guard.ts (exported: assertReadableQueryFields, collectQueryFields, collectConditionFields), wired as step 2.9 of the existing middleware.

Verification

  • @objectstack/spec: full suite 6694/6694 (62 in app.test.ts incl. 4 new); build + gen:api-surface clean (no surface diff)
  • @objectstack/plugin-security: 183/183 (10 new predicate-guard cases) after turbo build of the dependency graph
  • Changeset: minor for both packages, with the FROM → TO migration note for the exclusivity tightening

Follow-up (noted, not included)

A packages/verify filter-oracle invariant (probe a hidden field via predicate as a low-privilege member, assert 403/no-influence — the rls.ts #1994 pattern) would give this end-to-end regression coverage per app; kept out to keep this PR focused on the enforcement itself.

🤖 Generated with Claude Code

https://claude.ai/code/session_018m3GX7EMKNPDZuee152EKK


Generated by Claude Code

…cate guard

Two guards completing objectui#2251 / objectui ADR-0055:

spec — NavigationItemSchema rejects object items combining filters with
recordId or viewName (superRefine on the union member; base schema stays
extendable). Runtime precedence would silently ignore the extras — a
stale recordId hijacking a configured filters slice — so the ambiguous
shape is now unwritable (ADR-0053 correct-by-construction). The legacy
recordId+viewName combination stays tolerated (documented). 4 new
schema tests; api-surface unchanged.

plugin-security — anti filter-oracle predicate guard. FieldMasker only
masks RESULTS; filtering/sorting/grouping/aggregating by a hidden field
still leaked its values through row presence. The middleware now
rejects (403, reason: field_predicate_denied) caller queries whose
where/orderBy/groupBy/having/aggregations/windowFunctions reference a
non-readable field — evaluated against the caller's AST BEFORE RLS
injection so RLS policies may keep referencing hidden fields. Rejection
over silent dropping: removing an $and branch widens results and
re-opens the oracle (Salesforce FLS errors the same way). 10 new unit
tests; plugin-security suite 183/183.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018m3GX7EMKNPDZuee152EKK
@vercel

vercel Bot commented Jul 5, 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 5, 2026 8:40am

Request Review

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

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-security, @objectstack/spec.

96 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/cloud-artifact-api.mdx (via packages/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security, @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/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/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-security, packages/spec)
  • content/docs/permissions/permission-sets.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/permissions/profiles.mdx (via @objectstack/spec)
  • content/docs/permissions/roles.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • 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-security, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.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 packages/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-security, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/role-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/setup-app.mdx (via @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.

…ion layer table

Layer 6 (field-level security) now states the predicate guard: caller
queries filtering/sorting/grouping/aggregating by a non-readable field
are rejected (403 field_predicate_denied) rather than value-leaking
through row presence; RLS-injected predicates are exempt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018m3GX7EMKNPDZuee152EKK
@os-zhuang
os-zhuang marked this pull request as ready for review July 5, 2026 08:50
@os-zhuang
os-zhuang merged commit 3962023 into main Jul 5, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/spec-filters-exclusivity branch July 5, 2026 08:51
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants