Skip to content

fix(verify): provision the RLS probe persona through the registry, not a set row (#15204 stage 4) - #22807

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-15204-s4-verify-registry-persona
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-15204-s4-verify-registry-persona

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15204
Clause-②: no

Stage 4 of #15204 (stage plan 6094501866): verify provisions its RLS probe persona through the registry, not a set row. Claimed in the amendment 6106681412. The card stays open; its other stages are untouched here.

What changes

packages/verify/src/rls.ts, provisionRlsProbePersona:

  • Before: it found or created a sys_permission_set row named verify_rls_probe. When the row was missing, it minted one with the probe's object permissions and RLS serialized into row columns, then granted it.
  • After: it mints no permission set.
    1. It asks the security catalog, through securityCatalogReaderOf(ql).resolve('permission', 'verify_rls_probe'). That is the read the authorization resolver itself uses (ADR-0131 D3/D4). rlsProbeSecurity(config) registers the set at boot. A stack booted without it now fails here, loudly, instead of granting a name that resolves to nothing.
    2. It reads the id of the sys_permission_set row the boot already seeded for that registered set. sys_user_permission_set.permission_set_id is still a required column (it goes in stage 8 or C7b), and the platform derives the grant's name from it and refuses a disagreement (grant-permission-set-name.ts). This is no transition piece: no new read or write is added for an intermediate state. It is the one remaining existing read, narrowed from find-or-create to find.
    3. It writes the one grant, carrying both columns as before.
  • Either missing piece (no catalog entry, or no seeded row) throws and writes nothing. objectstack verify --rls already turns any provisioning failure into probe.degraded plus a hard failure, so neither case can pass as a weaker persona.

Measured before building (origin/main 9f5eca52b)

  • The boot seeds the row. SecurityPlugin passes options.defaultPermissionSets (which rlsProbeSecurity extends with the probe set) as bootstrapPermissionSets to bootstrapPlatformAdmin at kernel:ready. That function inserts a row for every bootstrap set it does not find (bootstrap-platform-admin.ts around line 873).
    • A real bootStack(showcase, { security: rlsProbeSecurity(config) }) held exactly one verify_rls_probe row afterwards: organization null, managed_by: platform.
  • The catalog serves the set. On the same boot, securityCatalogReaderOf(ql).resolve('permission', 'verify_rls_probe') answered an entry with 24 objects and 24 rowLevelSecurity rules. The control, an unregistered name, answered undefined.
  • Multi-tenant was not measured. A multi-tenant boot needs the enterprise @objectstack/organizations package, which this repository cannot load. The PR's CI is not shown to cover it either: no test drives verify --rls end to end.

Verification

All at 72cc4a67c8:

  • Build: pnpm --filter '@objectstack/example-showcase^...' --filter '@objectstack/cli...' build exited 0, as did the @objectstack/dogfood^... build.
  • Unit tests: pnpm --filter @objectstack/verify test passed 29 files, 236 tests. pnpm --filter @objectstack/verify typecheck was clean, including check:test-typecheck.
  • rls-persona-grant.test.ts, rewritten. It pins four cases: the registered set is granted by name with the seeded row's id and no set row is inserted; no seeded row refuses and writes nothing; a row with no catalog entry refuses and writes nothing; no catalog bound refuses and writes nothing.
  • Reverse verification (one-off, not kept):
    • main's rls.ts was restored under the new test with git restore --source=9f5eca52b. The mutant held genId('ps') once and securityCatalogReaderOf zero times. The result was 3 failed, 1 passed: every refusal case went red, and the seeded-row case stays green because main also reuses a found row.
    • Restored with git checkout HEAD. Blob edefdfbc2589 matched HEAD, and git diff HEAD was empty.
  • End to end: objectstack verify --app ./objectstack.config.ts --rls --json was run on examples/app-showcase, twice on main's code and twice on this branch.
    • All four runs had hardFailures: 0 and a probe that was not degraded (grantedObjects: 24).
    • All four had identical totals: 264 objects, 38 consistent, 0 holes, 131 member-visible, 62 probe-blocked, 33 skipped.
    • All four had identical per-object and per-position statuses.
    • The only difference was one run's probe target for showcase_invoice, adopted rather than created after an admin create answered 400. The second run on this branch created it again, so this is run-to-run noise.
  • Dogfood suite, in full (pnpm --filter @objectstack/dogfood test; its files import @objectstack/verify): 245 files passed, 1 skipped; 1977 tests passed, 9 skipped; exit 0.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE derived 61; 60 ran green and 1 is NOT MEASURED. That one is check:dual-build-cjs-loads, which exited 3 (PREREQUISITE NOT MET: it needs a whole-repo build) and is left to CI.

Landing order with S5c

#22802 (#15196 S5c, comments only) edits the comment this PR replaces. Branching rules forbid stacked series, so this branch is cut from main, not from S5c. Whichever of the two lands second takes one textual conflict in provisionRlsProbePersona's comment block, resolved by keeping this PR's text. #22802 also corrects rlsProbeSecurity's docblock, which this PR leaves untouched for that reason.

Acceptance notes

  • Carried by stage 6b-2: this read depends on the boot seeding a sys_permission_set row for every registered bootstrap set (bootstrapPlatformAdmin). Stage 6b deletes "platform-admin materialization". If 6b-2 lands before stage 8 or C7b drops the permission_set_id requirement, it must carry this read along with the other grant writers that read a seeded id. Otherwise verify --rls reports a degraded persona, loudly, which is the failure mode this PR chose over minting.

Generated by Claude Code

…t a set row

provisionRlsProbePersona no longer mints a sys_permission_set row. It asks
the security catalog, the same read the authorization resolver uses,
whether the probe set is registered (rlsProbeSecurity registers it at
boot). It reads the id of the row the boot seeds for that registered set,
because sys_user_permission_set.permission_set_id is still required, and
writes the one grant. A probe set the catalog does not serve, or a missing
boot-seeded row, refuses and writes nothing.

Claude-Session: https://claude.ai/code/session_01M5baMwzrfukawVWc496yxD
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/verify, touching 3 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/concepts/metadata-lifecycle.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/deployment/environment-variables.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/permissions/authorization.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/permissions/delegated-administration.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/permissions/permission-sets.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/releases/v13.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/releases/v15.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/releases/v17/17-0.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/releases/v17/17-1.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/releases/v17/17-5.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))
  • content/docs/releases/v17/17-6.mdx (via sys_permission_set (literal, a string literal in provisionRlsProbePersona))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 14f4912390d7761a4b91a6e2ed1f1ca1fb95adfe → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9b6205a393c5738ab2b9c27d8d04267788eebead — the merge of head ca4b3380b8d9e6ce50f59bb14bef6618fc36a6e4 into base 14f4912390d7761a4b91a6e2ed1f1ca1fb95adfe, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9b6205a393c5738ab2b9c27d8d04267788eebead && git checkout 9b6205a393c5738ab2b9c27d8d04267788eebead
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 14f4912390d7761a4b91a6e2ed1f1ca1fb95adfe ca4b3380b8d9e6ce50f59bb14bef6618fc36a6e4 && git checkout -B drift-repro 14f4912390d7761a4b91a6e2ed1f1ca1fb95adfe && git merge --no-ff ca4b3380b8d9e6ce50f59bb14bef6618fc36a6e4

node scripts/docs-audit/affected-docs.mjs --json 14f4912390d7761a4b91a6e2ed1f1ca1fb95adfe

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 14f4912390d7761a4b91a6e2ed1f1ca1fb95adfe → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Brings in #22802 (S5c of #15196). One conflict, in provisionRlsProbePersona's
comment block: kept this branch's text (the find-or-create block it
annotated is gone here). rlsProbeSecurity's docblock correction from #22802
is kept as merged.

Claude-Session: https://claude.ai/code/session_01M5baMwzrfukawVWc496yxD
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Stage 4 (#22807): merge round checked; queued · epic PM session_01Rerax7QTjKMPCUZxQUtPFR · 2026-10-11T09:26Z


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 09:27
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 09:27
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit e078506 Oct 11, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-15204-s4-verify-registry-persona branch October 11, 2026 09:50
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