Skip to content

chore(spec): scan platform-object definitions for permissive empty states (#3896 follow-up) - #3966

Merged
os-zhuang merged 1 commit into
mainfrom
claude/sharing-rules-schema-bypass-h4c7xr
Jul 30, 2026
Merged

chore(spec): scan platform-object definitions for permissive empty states (#3896 follow-up)#3966
os-zhuang merged 1 commit into
mainfrom
claude/sharing-rules-schema-bypass-h4c7xr

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Follow-up to #3945 (merged). That PR built a gate requiring any "empty = permissive" statement in the spec to be classified on purpose. Its scan scope had a hole big enough to miss the bug it was built for.

The gate could not see its own crime scene

The sentence that shipped #3896"leave empty to share every record" — was the description of sys_sharing_rule.criteria_json. That field lives in plugin-sharing, not in packages/spec. #3945 scanned packages/spec/src/**/*.zod.ts only.

So the ratchet was real but it was pointed slightly away from where the class actually lives: platform-object field descriptions are the surface authors and models read most directly, and they sit in plugins.

Now scans **/*.object.ts anywhere under packages/ — plugins, platform-objects, metadata-core, and the create-objectstack templates (a starter file is the highest-leverage thing a model copies from). 214 → 290 files.

It immediately found a real one

sys_user_permission_set.organization_id:

description: 'Optional organization scope. NULL = applies in every org context.'

A user↔permission-set grant with no org scope applies everywhere. That is deliberate and load-bearing rather than an oversight, and the reason is worth stating precisely: ADR-0095 D3 / ADR-0068 D2 derive the platform_admin posture from an unscoped admin_full_access user grant specifically, and an org-scoped grant of the same set must not confer it.

So the empty state here is not merely wider — it is the distinguishing input to the highest privilege in the system. Registered open with that rationale and both enforcement sites cited (resolveAuthzContext.hasPlatformAdminGrant as the single source of truth; explain-engine.ts, which replicates the identical predicate on purpose so the explain panel cannot sit higher than enforcement). The point is that the answer now lives somewhere other than a maintainer's memory.

No behaviour is changed here — this is a classification, and the classification is that the current behaviour is intended.

Three fixes the new surface forced

Each was the gate being wrong in a way that mattered, and each was found by running it rather than by reasoning about it.

1. Repudiated prose no longer fires. #3929's own comment on criteria_json reads:

// Deliberately NOT "leave empty to share everything" (#3896): an empty

The gate flagged the sentence recording why the gate exists. Negation now covers the imperative form as well as the token form (deny-all, which had the same bug in #3945 and was fixed there). The escape is deliberately narrow — the negator must be attached to the phrase, not merely present in the line, so "If not set, leave empty to share every record" is still caught. A false negative here is a missed over-share, which is strictly worse than a false positive; there's a test pinning both directions.

2. The owning property is found by nesting, not by a list of key names. A platform-object field is a nested call, so the first attempt answered description for every hit — a name that is not a property and would key every entry in a file to the same string. Adding a doc-slot skip list then answered required: the sibling config key above it. What actually separates a field from its own config is indentation, so the resolver takes the nearest key at a shallower indent. Direction matters too — a JSDoc block sits at the same indent as the property below it, so forward search allows equality where backward search must not.

3. The property-search window is per-surface. A platform-object description: can sit 15+ lines below its field name; a .zod.ts statement sits on or beside its property. Widening globally would let .zod.ts narrative be mis-attributed to a distant property — turning a correct non-failing note into a wrong failure — so .object.ts gets a wider window and the schema surface keeps its tight one.

Also: evidence resolution is now honest

Entries are parsed with the liveness ledger's own checkEvidence, so prose around the paths works, several paths can be cited, and another repo's path (objectui: …) is recorded without being resolved here. #3945's README already promised evidence resolved "like the ledger's" — a single raw-path existsSync quietly did not, and would have rejected exactly the prose-plus-paths form the ledger documents.

Verification

  • 6 new unit tests (32 total) covering the platform-object field block, the repudiated-phrase escape and the case it must not suppress, and the per-surface window.
  • packages/spec: 263 files, 6862 tests, all passing. tsc --noEmit clean.
  • Both gates green — check:empty-state reports 20 statements across 290 files, all classified (1 closed, 2 open, 4 output, 9 scope); check:liveness unaffected.
  • No runtime behaviour changes.

Still not in scope

  • Locale bundles. Field descriptions are translated (translations/*.objects.generated.ts), so a permissive-empty claim could survive in a non-English bundle after the source is fixed. Those files are generated, so the right fix is at the generator, not the gate — a separate change.
  • Decoupling required (write-time) from NOT NULL (storage), still the outstanding ergonomics item behind fix(sharing)!: a rule with no criteria shares NOTHING, not every record (#3896) #3929's imperative write guards.

Generated by Claude Code

…ates (#3896)

#3945 added a gate requiring any "empty = permissive" statement in the spec to be
classified on purpose. It scanned packages/spec/src/**/*.zod.ts — and that scope
had a hole big enough to miss the bug it was built for.

The sentence that shipped #3896, "leave empty to share every record", was the
`description` of `sys_sharing_rule.criteria_json`, which lives in plugin-sharing.
The gate could not see its own crime scene.

It now scans **/*.object.ts anywhere under packages/ — plugins, platform-objects,
metadata-core, and the create-objectstack templates, since a starter file is the
highest-leverage thing a model copies from. 214 → 290 files.

It immediately found a real one. sys_user_permission_set.organization_id declares
"NULL = applies in every org context": a user↔permission-set grant with no org
scope applies everywhere. Deliberate and load-bearing rather than an oversight —
ADR-0095 D3 / ADR-0068 D2 derive the platform_admin posture from an UNSCOPED
admin_full_access grant specifically, and an org-scoped grant of the same set must
not confer it. The empty state is not merely wider; it is the distinguishing input
to the highest privilege in the system. Registered `open` with that rationale and
both enforcement sites cited — the point being that the answer now lives somewhere
other than a maintainer's memory.

Three fixes the new surface forced, each the gate being wrong in a way that mattered:

- Repudiated prose no longer fires. #3929's own comment reads Deliberately NOT
  "leave empty to share everything" — the gate flagged the sentence recording why
  the gate exists. Negation now covers the imperative form as well as the token
  form (deny-all), and the escape is narrow on purpose: the negator must be
  attached to the phrase, not merely present in the line, because a false negative
  here is a missed over-share.
- The owning property is found by nesting, not a name list. A field's prose sits in
  a nested key, so the first attempt answered `description` for every
  platform-object hit; skipping doc slots then answered `required`, the sibling
  above it. What separates a field from its own config is indentation.
- The property-search window is per-surface. A platform-object description: can sit
  15+ lines below its field name, while a .zod.ts statement sits beside its
  property. Widening globally would let .zod.ts narrative be mis-attributed to a
  distant property — a correct note becoming a wrong failure.

Evidence resolution is now honest too: entries are parsed with the liveness
ledger's own checkEvidence, so prose works, several paths can be cited, and
another repo's path is recorded without being resolved here. The README already
promised evidence resolved "like the ledger's"; a raw-path existsSync quietly did
not.

6 new unit tests (32 total). spec: 263 files / 6862 tests green, tsc clean, both
gates green. No runtime behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX
@github-actions github-actions Bot added documentation Improvements or additions to documentation size/m tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

105 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/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @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/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/spec)
  • 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/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @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/spec)
  • content/docs/plugins/packages.mdx (via @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/index.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 @objectstack/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/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/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @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/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 00:21
@os-zhuang
os-zhuang merged commit 7cb922e into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/sharing-rules-schema-bypass-h4c7xr branch July 30, 2026 00:21
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