Repository navigation
Commit 17e4f52
feat(spec)!: retire rowLevelSecurity[].tags — no mainstream platform tags a row-level policy (ADR-0049) (#20353)
Fixes #20321
Clause-②: no (narrowing)
## What this does
Retires `rowLevelSecurity[].tags` (`RowLevelSecurityPolicySchema.tags`)
by the `spec-property-retirement` route. It is ADR-0049
enforce-or-remove, graded **RETIRE** by triage (`5860425529`) under the
maintainer's criterion on #18900 (`5727134555`). Triage's verdict,
verbatim: 「Row-level policies carry no tag attribute in the mainstream:
Salesforce sharing rules, Dataverse security roles and PostgreSQL RLS
policies. Compliance reporting keys on the rule itself. ⇒ **RETIRE**.」
Retirement is immediate, with no staged window.
The Clause-② line is the seat's measured answer, `no (narrowing)`, which
supersedes the claim's `yes` (`5860732909`, triage's execution note).
`scripts/pm/clause2-line.mjs` defines the value as the answer to
「本卡放宽接受集或扩大公开面吗」. Nothing widens: the tombstone narrows the accept set,
the D2 conversion only heals stored rows the load path already accepted,
and no export is added. A policy that carried `tags` parsed before this
change and is refused after it, which is the `(narrowing)` arm. The
changeset carries the same line.
## Accept/refuse changes (all pinned)
| input | before | after | pin |
|:--|:--|:--|:--|
| `RowLevelSecurityPolicySchema` with `tags` (any value, `[]` included)
| parsed, stored, read by nothing | refused: `invalid_type` at
`['tags']`, with the prescription | `rls-tags-retirement.test.ts`,
`rls.test.ts` |
| `permission` write door (`getMetadataTypeSchema('permission')` =
`PermissionSetSchema`) with a policy carrying `tags` | accepted |
refused: `invalid_type` at `['rowLevelSecurity', 0, 'tags']`, with the
prescription | `rls-tags-retirement.test.ts` |
| `defineStack` with such a permission set | accepted | refused:
`STACK_SCHEMA_INVALID` / `422`, issue at `['permissions', 0,
'rowLevelSecurity', 0, 'tags']`, with the prescription |
`rls-tags-retirement.test.ts` |
| the same policy / set / stack **without** `tags` | accepted |
accepted, byte-identical output, no `tags` materialized | CONTROL cases
in the same file |
| a near-miss `tag` | refused as unknown; the did-you-mean offered
`tags` | refused as unknown (`unrecognized_keys`); the tombstone is
never offered (`acceptsNothing`) | `rls-tags-retirement.test.ts` |
| a stored permission row carrying `tags` | stored verbatim | stripped
on rehydration by the D2 `permission-rls-tags-removed`, then accepted by
the write door | `rls-tags-retirement.test.ts` |
### The new refusal text, verbatim (`RLS_POLICY_TAGS_RETIRED` in
`rls.zod.ts`)
> `rowLevelSecurity[].tags` was removed in @objectstack/spec 17.5.0
(ADR-0049 enforce-or-remove) — nothing ever read it: the RLS compiler
never consulted a policy's tags and nothing else acted on them, so a tag
scoped, restricted and reported nothing. Delete the key. A tag never
limited whom a policy applies to; to do that, list the positions in
`positions`. A policy is identified by its `name` and its `object`; say
why it exists in `description`. Run `os migrate meta --from 17` to list
the mechanical edits for existing sources; apply them by hand.
The generated `.describe()` is that text prefixed with `[REMOVED] ` (the
`retiredKey()` helper). It replaces `Policy categorization tags` in
`content/docs/references/security/rls.mdx` and `permission.mdx`. A
repo-wide grep for the old describe string finds no pin anywhere.
## Measured before changing anything (Zone 2 of the dispatch)
Each reading has a lit control on the same ref.
| where | ref | readers of a policy's `tags` | writers of a policy's
`tags` | lit control |
|:--|:--|:--|:--|:--|
| objectstack `plugin-security` / `lint` / `rest` / `objectql` /
`runtime` / `metadata*` / `services` src | `a78f731a` | 0. The only
`tags` hits are record-field CEL (`size(record.tags)`), OpenAPI route
`tags`, and the docs-audience `d.tags` in `meta-item-read-gate.ts`, and
none of them is a policy. | 0 in `examples/**` and in the
plugin-security producers (`default-permission-sets.ts`,
`bootstrap-platform-admin.ts`, `platform-*-policies.ts`,
`permission-set-projection.ts`, `security-plugin.ts`) | `.positions`
read 35 times in plugin-security src; `rowLevelSecurity` present in all
7 producer files |
| objectui at the `.objectui-sha` pin | `f8a9d0fb` | 0.
`PermissionAdvancedFacets.tsx` has 0 `tags` (its seed is
`{name,object,operation,using,check,enabled}`, and `RETIRED_RLS_KEYS` is
`['priority']`). `PermissionPreview.tsx` renders `${rls.length} RLS
rules`. `permission-slice.ts` / `clientValidation.ts` have 0. | 0 |
facet: `.using` 3, `.enabled` 2, `priority` lit |
| objectui `main` | `972c1685` | 0 in all 3 non-test RLS files | 0 |
facet: `priority` 3, `.using` 3 |
| cloud `main` | `96eb092f` | 0 | 0 (`apps/ee-group-showcase` security
sources) | `rowLevelSecurity` 1 in each file |
So the dispatch's mechanism assumptions 1 and 2 hold. On assumption 3:
`RowLevelSecurityPolicySchema` is a `strictObject`, so this is not the
ADR-0104 silent-strip case. The def is reachable from the `permission`
root, and the precedent one key over (`priority`) is a `retiredKey()`
tombstone on this same closed shape. That tombstone route keeps the
liveness row, which stays `dead`.
## What changed
- `packages/spec/src/security/rls.zod.ts`: `tags` becomes
`retiredKey(RLS_POLICY_TAGS_RETIRED)`. The const is declared above the
lazy schema, per the `OS_EAGER_SCHEMAS` TDZ rule. The docblock records
the census and the mainstream reading. The suggestion-pool comment now
names both tombstones.
- ADR-0087:
- D2 conversion `permission-rls-tags-removed` in
`conversions/registry.ts` (`toMajor: 18`, `retiredFromLoadPath: true`).
It is a `stripKeys` delete over `permissions[].rowLevelSecurity[]`,
copy-on-write, with a fixture of 1 notice that is disjoint from every
other entry.
- The D2 is wired into `MIGRATIONS_BY_MAJOR[18].conversionIds`, and the
step rationale is extended.
-
`migrations/entries/retired-keys/18.security__RowLevelSecurityPolicy__tags.ts`
holds the exact key `security/RowLevelSecurityPolicy:tags`.
- The family D3 entry is
`migrations/entries/semantic/18.permission-rls-tags-retired.ts`, per
ruling B on #17152: one D3 entry per retirement family, even when D2 is
lossless.
- `registry.ts` generated regions were regenerated with
`gen:migration-registry`. `spec-changes.json` and the upgrade guide are
byte-identical, because major-18 entries do not project yet
(`check:spec-changes` / `check:upgrade-guide` green).
- Liveness: `liveness/permission.json`'s `rowLevelSecurity.tags` row
stays `dead` under its tombstone. It gets `verifiedAt: 2026-09-27`,
evidence pointing at the tombstone, and a REMOVED note in the `priority`
row's house style. The `permission` row of `liveness/README.md` gains
one sentence. Counts are unchanged, because the row goes dead to dead.
- Generated: `authorable-surface/security.json` now reads
`security/RowLevelSecurityPolicy:tags [RETIRED]`. Two reference pages
were regenerated (`check:generated` named only `check:docs` stale).
- Tests:
- `rls.test.ts` fixture triage:
- Two incidental authorings were dropped: the complete-policy and
multi-tenant cases.
- `should validate tags` was **replaced** by a refusal pin, because it
pinned exactly the retired branch.
- The GDPR case now asserts that the purpose lives in `description` and
in the predicate, and that no `tags` comes back.
- The minimal-policy case asserts that no `tags` is materialized.
- New `rls-tags-retirement.test.ts` (14 cases) covers every door above,
the tsc channel (`@ts-expect-error`, compiled by `tsconfig.test.json`),
the D2 (stored-row rehydration, per-policy scope, measured idempotence,
load-path retirement) and the registration. It also has a **tree-scoped
absence** leg with a structural matcher: an object or YAML mapping whose
own keys include `tags`, `operation` and `using`/`check`. The leg has an
anti-vacuity battery over 14 specimens and walks the 5 roots already
declared for `@objectstack/spec#test`. It is listed in
`vitest.repo-tests.json`.
- `.changeset/20321-rls-policy-tags-retired.md`: `@objectstack/spec`
`minor`, with a **BREAKING** banner, FROM → TO, and the ADR-0087
disposition `registered permission-rls-tags-removed,
permission-rls-tags-retired`.
No form or i18n edit: `permission.form.ts` edits `rowLevelSecurity` as
one `json` widget, with no per-key input. No skill teaches the key.
There is no hand-written doc mention:
`content/docs/permissions/rls.mdx:231`'s "tags each with `kernelTier`"
is prose about the explain engine. No objectui change is needed: the pin
reads nothing of the key and does not import
`RowLevelSecurityPolicy['tags']`.
## Verification, at `62fd232a73`
Heavy runs went through `scripts/pm/os-verify-lock.sh`. The spec `dist`
was rebuilt at this head before any dist-reading gate.
- `pnpm --filter @objectstack/spec build`: VERDICT command-exit 0, tree
clean afterwards (no artifact drift).
- `pnpm --filter @objectstack/spec run typecheck` → exit 0.
`check:test-typecheck`: 53 files, 255 errors, 142 pinned signatures
held. So the `@ts-expect-error` compiles in a real program.
- `vitest run --project local`: exit 0, **554 files / 16357 tests
passed**, 1 todo.
- `vitest run --project repo`: exit 0, **34 files / 618 tests passed**.
This includes the new retirement file (14/14).
- Consumer suites (contract-face fixture triage), after building their
closures:
- `@objectstack/lint` `vitest run`: exit 0, **111 files / 4304 tests**.
- `@objectstack/plugin-security` `vitest run`: exit 0, **141 files /
2990 tests**.
- **Reverse verification** (one-time, no file left behind; a trap
deletes the probe; `git status` is clean afterwards): a probe in
`plugin-security/src` resolves `@objectstack/spec/security` through its
exports to the **built** `dist/security/index.d.mts`.
- Typing `{ …, tags: ['gdpr'] }` as `RowLevelSecurityPolicy` → exit 2,
`error TS2322: Type 'string[]' is not assignable to type 'undefined'` at
the `tags` column.
- The same literal without `tags` → exit 0.
- The expected direction was red, and red was observed.
- The first attempt was a null op, stated rather than hidden: TS 6
refused the command-line file with TS5112 until `--ignoreConfig` was
passed.
- The dispatch gates were re-derived on the actual change set
(`dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at
`62fd232a73`, 113 lines). All were run with exit codes recorded, then
reconciled: `--ran` → **113 derived, 112 run, 1 NOT-MEASURED, 0 UNRUN**.
All 112 run exit 0. That includes `check:liveness` (86 tombstones
reached, all graded with an allowed status), `check:generated` (15/15
current), `check:migration-registry`, `check:spec-changes`,
`check:upgrade-guide`, `check:adr-0087-registration --base origin/main`,
`check:changeset-no-major`, `check:authorable-surface`,
`check:api-surface`, `check:doc-authoring`,
`check:cross-package-test-inputs`, `check:nul-bytes` and
`check:type-check-debt` (re-measure, 4 entries, none above record).
- The roster gates whose roster sits under a changed directory were also
run: `check-changeset-fixed`, `check:meta-url-spelling`,
`check:authz-resolver`, `check:error-code-casing` and
`check:filter-alias-parity`. All exit 0.
NOT MEASURED: `check:dual-build-cjs-loads`. Reason: PREREQUISITE NOT
MET, exit 3. It loads every package's built entry, and 38 workspace
packages are unbuilt in this worktree (apps, connectors, most services).
That is a whole-workspace build, which is CI's `Build Core` job.
NOT MEASURED: `packages/cli` `integration` tier
(`migrate-meta.e2e.test.ts` replays the chain). This diff touches no
`bin/` or spawn entry, so it is declared to CI.
NOT MEASURED: `packages/qa/dogfood` expression conformance. `tags`
carries no expression surface, and the ledger's RLS `covers` rows name
only `.using` / `.check`.
`main` gained one commit (`d3958bac`, driver-sql only) after the base
`a78f731a`. It is disjoint from this diff and was not merged in.
## Acceptance notes (observations, not filed)
- objectui's RLS facet strips only `RETIRED_RLS_KEYS = ['priority']` on
load. A stored permission row carrying `tags` is healed before it
reaches the editor, because the D2 replays at rehydration, so no path to
a refused save is measured. This is an inference only. Carrier: none.
- `.claude/skills/spec-property-retirement` §0 still says benign display
metadata (`description`, `tags`, `icon`) should never be retired. This
card was graded RETIRE under the maintainer's later #18900 criterion,
which asks about the mainstream capability rather than readers. That
skill line now contradicts ruled practice for this family. It is a
governed surface and the PM's call. Carrier: none.
- `authorable-surface.base.json` still lists the key without
`[RETIRED]`. That is by design: only `gen:authorable-surface-base`
writes that file, and `check:authorable-surface` is green.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent db74b16 commit 17e4f52
14 files changed
Lines changed: 858 additions & 28 deletions
File tree
- .changeset
- content/docs/references/security
- packages/spec
- authorable-surface
- liveness
- src
- conversions
- migrations
- entries
- retired-keys
- semantic
- security
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
175 | | - | |
| 175 | + | |
176 | 176 | | |
177 | 177 | | |
178 | 178 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
183 | | - | |
| 183 | + | |
184 | 184 | | |
185 | 185 | | |
186 | 186 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | | - | |
| 153 | + | |
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
906 | 906 | | |
907 | 907 | | |
908 | 908 | | |
909 | | - | |
| 909 | + | |
910 | 910 | | |
911 | 911 | | |
912 | 912 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
224 | 224 | | |
225 | 225 | | |
226 | 226 | | |
227 | | - | |
228 | | - | |
229 | | - | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
230 | 230 | | |
231 | 231 | | |
232 | 232 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11616 | 11616 | | |
11617 | 11617 | | |
11618 | 11618 | | |
| 11619 | + | |
| 11620 | + | |
| 11621 | + | |
| 11622 | + | |
| 11623 | + | |
| 11624 | + | |
| 11625 | + | |
| 11626 | + | |
| 11627 | + | |
| 11628 | + | |
| 11629 | + | |
| 11630 | + | |
| 11631 | + | |
| 11632 | + | |
| 11633 | + | |
| 11634 | + | |
| 11635 | + | |
| 11636 | + | |
| 11637 | + | |
| 11638 | + | |
| 11639 | + | |
| 11640 | + | |
| 11641 | + | |
| 11642 | + | |
| 11643 | + | |
| 11644 | + | |
| 11645 | + | |
| 11646 | + | |
| 11647 | + | |
| 11648 | + | |
| 11649 | + | |
| 11650 | + | |
| 11651 | + | |
| 11652 | + | |
| 11653 | + | |
| 11654 | + | |
| 11655 | + | |
| 11656 | + | |
| 11657 | + | |
| 11658 | + | |
| 11659 | + | |
| 11660 | + | |
| 11661 | + | |
| 11662 | + | |
| 11663 | + | |
| 11664 | + | |
| 11665 | + | |
| 11666 | + | |
| 11667 | + | |
| 11668 | + | |
| 11669 | + | |
| 11670 | + | |
| 11671 | + | |
| 11672 | + | |
| 11673 | + | |
| 11674 | + | |
| 11675 | + | |
| 11676 | + | |
| 11677 | + | |
| 11678 | + | |
| 11679 | + | |
| 11680 | + | |
| 11681 | + | |
| 11682 | + | |
| 11683 | + | |
| 11684 | + | |
| 11685 | + | |
| 11686 | + | |
| 11687 | + | |
| 11688 | + | |
| 11689 | + | |
| 11690 | + | |
| 11691 | + | |
| 11692 | + | |
| 11693 | + | |
| 11694 | + | |
| 11695 | + | |
| 11696 | + | |
| 11697 | + | |
| 11698 | + | |
| 11699 | + | |
| 11700 | + | |
| 11701 | + | |
| 11702 | + | |
| 11703 | + | |
| 11704 | + | |
| 11705 | + | |
| 11706 | + | |
| 11707 | + | |
| 11708 | + | |
| 11709 | + | |
| 11710 | + | |
| 11711 | + | |
| 11712 | + | |
| 11713 | + | |
11619 | 11714 | | |
11620 | 11715 | | |
11621 | 11716 | | |
| |||
11728 | 11823 | | |
11729 | 11824 | | |
11730 | 11825 | | |
| 11826 | + | |
11731 | 11827 | | |
11732 | 11828 | | |
11733 | 11829 | | |
| |||
0 commit comments