From f7649b0aa7d66c30a5f46b55a567de69ac9725d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 15:23:08 +0000 Subject: [PATCH 1/4] liveness(permission): rowLevelSecurity label/description are live at the objectui pin The permission editor's Row-Level Security section draws each policy's label and description (PermissionAdvancedFacets, objectui PR #11215), read at the .objectui-sha pin 89cad75d557. Both rows go dead -> live, citing the reader and the producer chain. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude --- packages/spec/liveness/permission.json | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/spec/liveness/permission.json b/packages/spec/liveness/permission.json index c88d80508f7..2906221a2b9 100644 --- a/packages/spec/liveness/permission.json +++ b/packages/spec/liveness/permission.json @@ -167,16 +167,20 @@ "note": "2026-08-28: RE-ANCHORED (commit 9ee2dcfbd) and REPOINTED — `rls-compiler.ts:203` had rotted onto a comment about failing closed when every applicable policy depended on an unpopulated `current_user.*` variable; the only read of this key is the warn line ~11 lines above it. Re-closed by hand against c459da6bc." }, "label": { - "status": "dead", + "status": "live", + "verifiedAt": "2026-10-03", "evidenceScope": "cross-repo", - "verifiedAt": "2026-08-10", - "note": "No reader reaches it, measured at objectui @db11afd4967. PermissionPreview.tsx#readPolicies reads each policy's `label`, and `PermissionPreview` draws it in its Row-Level Security list, but no route mounts that preview for `permission`. ResourceEditPage.tsx#MetadataResourceEditPage hands every non-create `permission` item to the custom EditPage that services/builtinComponents.tsx registers, `PermissionMatrixEditPage`. That page renders no preview, and its RLS form `PermissionAdvancedFacets` reads no policy label. Create mode previews only object, report and dataset. The other `getMetadataPreview` callers (`EmbeddedItemEditor`, `StudioDesignSurface`, the dev-only preview gallery) never open a `permission`. A preview no route mounts is a read point that never runs (README, 'Designer previews count as consumers'). Benign display metadata, deliberately NOT authorWarn'd. To re-open: a mounted surface that draws the policy's label." + "evidence": "objectui @89cad75d557: packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx#PermissionAdvancedFacets (the permission editor's Row-Level Security section heads each policy card with `pol.label`, verbatim: a plain spec string with no locale lookup, and a policy whose label is absent or empty draws nothing; the section opens collapsed, showing the policy count, until the author expands it)", + "producer": "objectui @89cad75d557: packages/app-shell/src/services/builtinComponents.tsx (registers `PermissionMatrixEditPage` as the `permission` EditPage; the later `anchors.ts` registration sets only `anchors`, and `registerMetadataResource` merges defined keys only, so that EditPage stands); objectui @89cad75d557: packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx#MetadataResourceEditPage (the element of both `metadata/:type/:name` routes in `console/AppContent.tsx`; it hands every non-create `permission` item to that EditPage); objectui @89cad75d557: packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx#PermissionMatrixEditPage (reads the set through `client.layered`, the `GET /meta/permission/:name/layers` route, and mounts `PermissionAdvancedFacets` on the whole served draft, `rowLevelSecurity` included; the Studio Access pillar in `studio-design/StudioDesignSurface.tsx` mounts the same page embedded); framework: packages/rest/src/meta-item-read-gate.ts#createMetaLayeredAnswer (serves every layer of a `permission` whole: `createMetaItemReadGate` has no per-type arm for it, and the ADR-0106 object-schema mask does not apply to a non-object type); framework: examples/app-showcase/src/security/permission-sets.ts#ContributorPermissionSet (an authored producer: each of its three policies declares a `label` and a `description`)", + "note": "RE-GRADED dead → live 2026-10-03 (#20299) under the #7131 previews ruling (README, 'Designer previews count as consumers'): for a display key, being shown to a human is the whole claimed effect, so the editor surface that draws it settles the row. No preview is cited, because `PermissionPreview`, which also draws it, is still mounted by no route for `permission`. objectui#11199, fixed by PR objectui#11215 (merge f8334f8777, an ancestor of the `.objectui-sha` pin 89cad75d557), added the caption. Read at that pin with `git show`. Control: at the superseded pin db11afd4967 the same file reads `pol.label` 0 times and `pol.description` 0 times, against `pol.name` once in both trees, which is the reachability gap the superseded note recorded (a preview that draws both, mounted nowhere). objectui#11215's booted probe on the showcase set `showcase_contributor` found both values on `/apps/studio/metadata/permission/:name`, `/apps/setup/metadata/permission/:name` and the Studio Access pillar; this re-grade is a static read and did not re-boot. UNCHANGED: benign display metadata, deliberately KEPT (ADR-0033) and not authorWarn'd. The flip ends the `liveness-dead-property` warning (#16094) an authored policy label drew." }, "description": { - "status": "dead", + "status": "live", + "verifiedAt": "2026-10-03", "evidenceScope": "cross-repo", - "verifiedAt": "2026-08-10", - "note": "Same closure as `label`, at objectui @db11afd4967: `PermissionPreview` draws each policy's `description` beneath its row, but no route mounts it for `permission`. `PermissionMatrixEditPage` takes the item, and its RLS form reads no description. Benign, not authorWarn'd." + "evidence": "objectui @89cad75d557: packages/app-shell/src/views/metadata-admin/PermissionAdvancedFacets.tsx#PermissionAdvancedFacets (draws `pol.description` beneath the policy's label in each Row-Level Security policy card, verbatim; an absent or empty description draws nothing)", + "producer": "objectui @89cad75d557: packages/app-shell/src/services/builtinComponents.tsx (registers `PermissionMatrixEditPage` as the `permission` EditPage; the later `anchors.ts` registration sets only `anchors`, and `registerMetadataResource` merges defined keys only, so that EditPage stands); objectui @89cad75d557: packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx#MetadataResourceEditPage (the element of both `metadata/:type/:name` routes in `console/AppContent.tsx`; it hands every non-create `permission` item to that EditPage); objectui @89cad75d557: packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx#PermissionMatrixEditPage (reads the set through `client.layered`, the `GET /meta/permission/:name/layers` route, and mounts `PermissionAdvancedFacets` on the whole served draft, `rowLevelSecurity` included; the Studio Access pillar in `studio-design/StudioDesignSurface.tsx` mounts the same page embedded); framework: packages/rest/src/meta-item-read-gate.ts#createMetaLayeredAnswer (serves every layer of a `permission` whole: `createMetaItemReadGate` has no per-type arm for it, and the ADR-0106 object-schema mask does not apply to a non-object type); framework: examples/app-showcase/src/security/permission-sets.ts#ContributorPermissionSet (an authored producer: each of its three policies declares a `label` and a `description`)", + "note": "RE-GRADED dead → live 2026-10-03 (#20299) with `label`, on the same measurement at the `.objectui-sha` pin 89cad75d557 (objectui#11199 via PR objectui#11215). Control at the superseded pin db11afd4967: `pol.description` 0 reads in the same file. The superseded note recorded that `PermissionPreview` drew it but was mounted nowhere, and that is still true; the facet is the reader. UNCHANGED: benign display metadata, KEPT (ADR-0033), not authorWarn'd." }, "object": { "status": "live", From e42810557dbfa7414a177eb0c257cc9da7c9404a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 15:24:09 +0000 Subject: [PATCH 2/4] liveness(permission): regenerate the permission count shard (38 live / 4 dead) gen:liveness-counts output for the two rowLevelSecurity rows that moved dead -> live. No shard hand-edited. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude --- packages/spec/liveness/state-counts/permission.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/liveness/state-counts/permission.md b/packages/spec/liveness/state-counts/permission.md index 98618ba725a..729ecb82e76 100644 --- a/packages/spec/liveness/state-counts/permission.md +++ b/packages/spec/liveness/state-counts/permission.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `permission` | 36 | 0 | 0 | 6 | 0 | 42 | +| `permission` | 38 | 0 | 0 | 4 | 0 | 42 | From 4d54abd0c86a2e7f2be67c96efa785e843e34466 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 15:29:00 +0000 Subject: [PATCH 3/4] test(lint): re-subject the #16094 dead-verdict pins to the view container's name/label permission.rowLevelSecurity.label / .description went live, so the two pins that used them as the authorable dead sample would fail. Their own comment prescribes the move: another dead row of a type the walk visits, authored by a fixture the shipped schema parses. The view container's own name and label are that row pair. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude --- .../lint/src/lint-liveness-properties.test.ts | 76 ++++++++----------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/packages/lint/src/lint-liveness-properties.test.ts b/packages/lint/src/lint-liveness-properties.test.ts index 4f8e844d175..f44d0def758 100644 --- a/packages/lint/src/lint-liveness-properties.test.ts +++ b/packages/lint/src/lint-liveness-properties.test.ts @@ -4,7 +4,6 @@ import { afterAll, describe, it, expect } from 'vitest'; import { cpSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { PermissionSetSchema } from '@objectstack/spec/security'; import { ViewSchema } from '@objectstack/spec/ui'; import { authorWarnedProperties, @@ -1632,45 +1631,40 @@ describe('a per-type ledger that could not be READ is reported once (#19276)', ( // These pins test it, against the real ledgers, so a ledger change that leaves // either id with no reachable row fails here by name. describe('the dead and live-elsewhere verdicts warn on their own (#16094)', () => { - // `rowLevelSecurity.label` / `.description` are `dead` rows (no mounted - // surface draws a policy's label or description) that an author really - // writes: the fixture parses through the shipped `PermissionSetSchema`, so - // neither key is a tombstone. If either row changes verdict, re-subject this - // pin to another `dead` row of a type the walk visits; if none is left, the - // rule id is unreachable again and that is what this pin exists to say. - const permissionSet = { - name: 'fx_reader', - label: 'Fx Reader', - objects: { fx_account: { allowRead: true, readScope: 'org' } }, - rowLevelSecurity: [ - // The first policy authors neither dead key, so the findings below can - // only come from the second: the dotted path fans out past index 0. - { name: 'fx_any_rows', object: 'fx_account', operation: 'select', using: 'name == current_user.email' }, - { - name: 'fx_own_rows', - label: 'Own rows', - description: 'Readers see their own rows.', - object: 'fx_account', - operation: 'select', - using: 'name == current_user.email', - }, - ], - }; + // The `view` container's own `name` / `label` are `dead` rows (Studio + // enumerates view items, never the aggregated container, so nothing draws or + // keys on them) that an author really writes: the fixture parses through the + // shipped `ViewSchema`, so neither key is a tombstone. If either row changes + // verdict, re-subject this pin to another `dead` row of a type the walk + // visits; if none is left, the rule id is unreachable again and that is what + // this pin exists to say. (It was re-subjected here when + // `permission.rowLevelSecurity.label` / `.description` went `live`, #20299.) + const list = (object: string) => ({ + type: 'grid', + data: { provider: 'object', object }, + columns: [{ field: 'name' }], + }); + const views = [ + // The first container authors neither dead key, so the findings below can + // only come from the second: the collection walk reaches past index 0. + { object: 'fx_account', list: list('fx_account') }, + { name: 'fx_contact', label: 'Contacts', object: 'fx_contact', list: list('fx_contact') }, + ]; it('the fixture is authorable — it parses through the shipped schema (the keys are not tombstones)', () => { - expect(PermissionSetSchema.safeParse(permissionSet).success).toBe(true); + for (const view of views) expect(ViewSchema.safeParse(view).success, view.object).toBe(true); }); it('END TO END: an authored dead key produces liveness-dead-property, and the live keys beside it stay silent', () => { - const findings = lintLivenessProperties({ permissions: [permissionSet] }); - expect(ruleOf(findings, 'rowLevelSecurity.label')).toBe(LIVENESS_DEAD_PROPERTY); - expect(ruleOf(findings, 'rowLevelSecurity.description')).toBe(LIVENESS_DEAD_PROPERTY); - // `live` is silent — same policy, same ledger load, so this is a verdict - // and not a walk that never ran. - for (const live of ['rowLevelSecurity.name', 'rowLevelSecurity.object', 'rowLevelSecurity.operation', 'rowLevelSecurity.using']) { + const findings = lintLivenessProperties({ views }); + expect(ruleOf(findings, 'name')).toBe(LIVENESS_DEAD_PROPERTY); + expect(ruleOf(findings, 'label')).toBe(LIVENESS_DEAD_PROPERTY); + // `live` is silent — same container, same ledger load, so this is a + // verdict and not a walk that never ran. + for (const live of ['object', 'list.type', 'list.data', 'list.columns']) { expect(ruleOf(findings, live), live).toBeUndefined(); } - expect(findings.map((f) => f.where)).toEqual(["permission 'fx_reader'", "permission 'fx_reader'"]); + expect(findings.map((f) => f.where)).toEqual(["view 'fx_contact'", "view 'fx_contact'"]); }); // The control the triage notes asked for: a TOMBSTONED key is still refused @@ -1838,20 +1832,16 @@ describe('the hint a warned row shows an author (#16094, #21096)', () => { } }); - it('END TO END: the authored dead RLS keys show the dead default hint, not the ledger note', () => { - const ledger = JSON.parse(readFileSync(join(shippedLedgerDir(), 'permission.json'), 'utf8')); - const rls = ledger.props.rowLevelSecurity.children; + it('END TO END: the authored dead view-container keys show the dead default hint, not the ledger note', () => { + const ledger = JSON.parse(readFileSync(join(shippedLedgerDir(), 'view.json'), 'utf8')); const findings = lintLivenessProperties({ - permissions: [{ - name: 'fx_reader', - rowLevelSecurity: [{ name: 'p', label: 'Own rows', description: 'Readers see their own rows.', object: 'fx_account' }], - }], + views: [{ name: 'fx_contact', label: 'Contacts', object: 'fx_contact' }], }); const deadDefault = checkItemAgainstWarnMap('gadget', { name: 'g1', gizmo: 'x' }, "gadget 'g1'", gizmoEntry({ status: 'dead' }))[0].hint; - for (const key of ['label', 'description']) { - const f = findings.find((x) => x.message.includes(`sets \`rowLevelSecurity.${key}\``)); + for (const key of ['name', 'label']) { + const f = findings.find((x) => x.message.includes(`sets \`${key}\``)); expect(f, key).toBeDefined(); - expect(f!.hint, key).not.toBe(rls[key].note); + expect(f!.hint, key).not.toBe(ledger.props[key].note); expect(f!.hint, key).toBe(deadDefault); } }); From 018e3971db616e712d34ebfe5b58c66dd8c32770 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 15:32:17 +0000 Subject: [PATCH 4/4] chore(spec): changeset and README permission cell for the RLS label/description re-grade The ledger ships in @objectstack/spec (files[] includes liveness), so the re-grade is a patch. The README's permission Notes cell enumerated these two keys in the dead set; it now records the re-grade and the dead set that remains. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude --- .changeset/20299-rls-policy-rows-live.md | 14 ++++++++++++++ packages/spec/liveness/README.md | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .changeset/20299-rls-policy-rows-live.md diff --git a/.changeset/20299-rls-policy-rows-live.md b/.changeset/20299-rls-policy-rows-live.md new file mode 100644 index 00000000000..5ee928b9a45 --- /dev/null +++ b/.changeset/20299-rls-policy-rows-live.md @@ -0,0 +1,14 @@ +--- +"@objectstack/spec": patch +--- + +Liveness ledger: a permission set's row-level security policy `label` and `description` (`rowLevelSecurity[].label` / `.description`) are now `live`, not `dead`. Studio's permission editor shows both on every policy. Ledger data and its generated count shard only. + +Clause-②: no + +- **What shows them.** These are display keys, so under the ledger's "Designer previews count as consumers" ruling, being shown to a human is the whole of their claimed effect. The Row-Level Security section of the permission editor (`PermissionAdvancedFacets` in objectui) now heads each policy card with the policy's `label` and, beneath it, its `description`, exactly as written. Both rows cite that reader at the `.objectui-sha` pin `89cad75d557`. The registered permission preview also draws both, but no route mounts it for `permission`, so it is not cited. +- **Where the values come from.** Each row names its producer: the `permission` edit page registration and the Studio edit route that mounts it, the editor's `GET /api/v1/meta/permission/:name/layers` read, and this repo's shared layered answer (`createMetaLayeredAnswer`), which serves a permission set whole. The showcase's contributor permission set authors both keys on all three of its policies. +- **Author-facing effect.** `os lint` / `os validate` no longer warn `liveness-dead-property` on a policy that sets `label` or `description`. A warning is not a refusal, so the accept set is unchanged. +- **Still kept.** The re-grade reverses no ADR-0033 decision. Both rows stay docs-shaped annotation, deliberately kept and not `authorWarn`'d. +- The regenerated liveness count is the `liveness/state-counts/permission.md` shard: `permission` has 38 live and 4 dead (was 36 and 6). The `view` container's own `label` stays `dead`. +- ⛔ No schema, parse, `.describe()`, export or accept-set change. diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index b05dfcd06c1..b55ed9b69e0 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -960,7 +960,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | flow | dead count = **5 tombstone entries**: `active`/`template`/nodes.`outputSchema`/errorHandling.`fallbackNodeId` REMOVED 2026-07-30 (#3896 close-out sweep — `active: false` never stopped a flow, `status` is the enforced lifecycle; faults route via per-node fault edges), plus errorHandling.`retryDelayMs` RENAMED to `backoffMs` 2026-08-04 (#4964). The rename is why the dead column moved while live did not: a rename is a removal on this ledger, so the old spelling is tombstoned (`retiredKey` keeps it in the walked shape) and the new spelling enters as its own `live` row. Read it beside the four above as the one entry here that cost an author nothing — the block was a THIRD encoding of the retry policy #4661 converged, invisible to that pass because it is an anonymous inline block with no exported name, and #4964 spelled its base delay `backoffMs` to match `job.retryPolicy` and a `try_catch` node's `retry`. `description` is the kept docs field — KEPT deliberately, docs-shaped, exempt from enforce-or-remove — and it left the dead set in #20299: the Studio metadata list's default columns and the metadata quick-find draw it for every flow, which for a display key is the whole of the claimed effect (the #7131 ruling above). Still not authorWarn'd | | action | `type:'form'` CORRECTED to live (objectui ActionRunner.executeForm, #2377); dead `timeout` REMOVED (#2377); `disabled` live since objectui#2863; `undoable` CORRECTED to live (#3714); `shortcut` + `bulkEnabled` REMOVED 2026-07-30 (#3896 close-out sweep — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions). **#7367** (PR #7430) adds `description` as an authorable key, `live` on arrival — the only row this type has gained since that sweep. **Commit cae2169cf** makes the dead set three: `execute` joins it, re-classified `live` → `dead` 2026-08-29 with no key added or removed. Its `live` verdict rested on a `.transform` lowering `execute` → `target` that protocol 17 (#3855) removed along with the alias; the key has been a `retiredKey` tombstone since 2026-07-28, so the row stays (the `rls.priority` precedent) while the verdict does not. The rot was invisible to every citation check — the pointer was in range, in the right file, and the file names the key — and the entry carried no `verifiedAt`, so nothing ever re-asked. **#20323** makes the dead set four: `aria` re-classified `live` → `dead` 2026-09-28 and tombstoned (the `rls.priority` precedent again). Its `live` verdict rested on an uncited 「PARTIAL — honored by a few objectui renderers」 note; re-measured at the `.objectui-sha` pin, no surface that renders an action reads an action's `aria`, and each takes the accessible name from the required `label` | | hook | model-healthy; label/description KEPT deliberately (2026-07-30 sweep) — docs-shaped annotation fields, exempt from enforce-or-remove — and **`live` since #20299**: `hook` has no registered preview, but the Studio metadata list's default columns and the metadata quick-find draw both keys for every hook, which for a display key is the whole of the claimed effect (the #7131 ruling above). Still not authorWarn'd | -| permission | CRUD/FLS/RLS live; dead `contextVariables` REMOVED (ADR-0105 D11 — RLS resolves only the `current_user.*` built-ins plus runtime-staged `rlsMembership` sets). 2026-07-30 security-subset re-verification (all 33 entries `verifiedAt`-stamped): `rowLevelSecurity.enabled` was live-with-wrong-evidence and UNREAD — a disabled policy kept contributing its OR-branch grant; ENFORCED same day in rls-compiler (`getApplicablePolicies`), the `positions` ADR-0049 resolution repeated. `rowLevelSecurity.priority` CORRECTED to dead+authorWarn — semantically void under OR-combination (no conflict exists to order), a REMOVE candidate. `rls.label`/`description`/`tags` CORRECTED to dead (benign display, no consumer in either repo). `tabPermissions` was UNDERSTATED ("only hidden read" → the rank merge reads all four values; me-apps dogfood test exercises it). `allowExport` re-verified TRUE end-to-end (server-side 403 gate, not just the /me projection). `objects.allowRestore`/`allowPurge` REMOVED 2026-08-26 (#12497, ADR-0049 — the `restore`/`purge` ops never existed; the 2026-07-30 'live' verdict cited only the evaluator pre-mapping, retired in the same batch; `retiredKey` tombstones, keys return with M2 per the #1883 ruling). `rowLevelSecurity.tags` REMOVED 2026-09-27 (#20321, ADR-0049 — graded RETIRE by the maintainer's criterion: no mainstream platform tags a row-level policy; a `retiredKey` tombstone, so the row stays `dead` beside `priority`'s) | +| permission | CRUD/FLS/RLS live; dead `contextVariables` REMOVED (ADR-0105 D11 — RLS resolves only the `current_user.*` built-ins plus runtime-staged `rlsMembership` sets). 2026-07-30 security-subset re-verification (all 33 entries `verifiedAt`-stamped): `rowLevelSecurity.enabled` was live-with-wrong-evidence and UNREAD — a disabled policy kept contributing its OR-branch grant; ENFORCED same day in rls-compiler (`getApplicablePolicies`), the `positions` ADR-0049 resolution repeated. `rowLevelSecurity.priority` CORRECTED to dead+authorWarn — semantically void under OR-combination (no conflict exists to order), a REMOVE candidate. `rls.label`/`description`/`tags` CORRECTED to dead (benign display, no consumer in either repo). `tabPermissions` was UNDERSTATED ("only hidden read" → the rank merge reads all four values; me-apps dogfood test exercises it). `allowExport` re-verified TRUE end-to-end (server-side 403 gate, not just the /me projection). `objects.allowRestore`/`allowPurge` REMOVED 2026-08-26 (#12497, ADR-0049 — the `restore`/`purge` ops never existed; the 2026-07-30 'live' verdict cited only the evaluator pre-mapping, retired in the same batch; `retiredKey` tombstones, keys return with M2 per the #1883 ruling). `rowLevelSecurity.tags` REMOVED 2026-09-27 (#20321, ADR-0049 — graded RETIRE by the maintainer's criterion: no mainstream platform tags a row-level policy; a `retiredKey` tombstone, so the row stays `dead` beside `priority`'s). `rowLevelSecurity.label` / `description` RE-GRADED `live` 2026-10-03 (#20299 — the permission editor's Row-Level Security section draws both on each policy card, read at the `.objectui-sha` pin 89cad75d557; still benign display, KEPT, not authorWarn'd), so the dead set is now `priority` and `tags` beside the two `objects.allowRestore` / `allowPurge` tombstones | | position | (role's ADR-0090 successor) fully live; all 4 `verifiedAt`-stamped 2026-07-30 | | agent | dead `tenantId` + `planning.strategy`/`allowReplan` REMOVED (#2377); the autonomy tier is experimental no longer — `structuredOutput` (#21277) and `memory` (#20274) flipped `live` on the cloud AI runtime's reading, and `lifecycle` REMOVED 2026-10-02 (#21320; ruled D on objectstack-ai/cloud#2569 — the conversation state machine was parsed and never read; phases are skills with `triggerConditions`, orchestration is Flow, record transitions are the `state_machine` validation rule; the XState `StateMachineSchema` family, which only it still reached, left with it), so no `agent` row is experimental; `knowledge` REMOVED 2026-07-30 (#3896 close-out sweep — declaring sources never scoped retrieval; AIKnowledgeSchema removed with it, the topics→sources rename absorbed pre-release); **#18304** re-classifies `tools` `live` -> `dead` with no key added or removed — the row asserted `live` on a key `agent.zod.ts` had tombstoned in protocol 17 (#3894), and it sat that way from the 2026-06 audit because its citation was EXEMPT from resolution rather than resolved (`packages/services/service-ai/...` matched `FOREIGN_PATH_PREFIXES`; the `cloud` realm marker that replaced it in #13309 is equally unresolvable, so no gate could ever fail on it). The load-bearing evidence is local and re-measurable — the `retiredKey` tombstone plus the `agent-tools-to-skills` strip cover authored and stored input respectively, so nothing can carry a value for any consumer to read; the cloud zero-consumer census (cloud @cb8ee7ff, #13272, 2026-09-15) is attributed, not re-taken. `live-elsewhere` is refused for want of a foreign enforcer, not left undeclared | | tool | the inert authoring surface is now REMOVED, not merely marked: `category`/`permissions`/`active`/`builtIn` retired 2026-07-30 (#3896 close-out) after `requiresConfirmation` set the precedent (#3715, ADR-0033 §2). `permissions` promised an invocation gate nothing enforced and `active:false` withdrew nothing — false compliance, same shape as rls.enabled. The `.strict()` ToolSchema rejects each retired key with its prescription; the `tool-inert-authoring-keys-removed` conversion strips them from authored sources |