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

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 (recordId → filters → viewName) 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

This branch was successfully deployed

1 active deployment
Preview — b67c0084 Deployed Jul 5, 2026 by vercel[bot]
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 size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants