Skip to content

docs(verify): the RLS probe grant holds its set by name, not by row id (#15196 S5c) - #22802

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-15196-s5c-verify-name-column
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-15196-s5c-verify-name-column

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15196
Clause-②: no

Stage S5c of #15196: verify reads the name column. Claimed by the epic PM lane in 6106687498 (the maintainer folded S5c in with 「并进来(推荐)」, recorded in #15204's amendment 6106681412). The card stays open; its other stages (S8a / S8b / S9) are untouched here.

What was left of S5c on main

Measured on origin/main 9f5eca52b:

  • No verify read keys a grant by row id. verify writes the probe grant and reads nothing back from grants. The writer already sends both columns (permission_set_id and the name column permission_set) since S4a (feat(plugin-security,plugin-auth,verify): sys_user_permission_set gains the permission-set name column, written by every grant writer (ADR-0131 C2 S4a) #22100).
  • The resolver reads the name column only. resolveUserAuthzGrants (packages/core/src/security/resolve-authz-context.ts, section 6) collects grantSetNameOf(grant), which reads permission_set alone, and takes each definition from the security catalog. Its comment states that "The grant's permission_set_id is not read".
  • No permission-set source reads a row. PermissionEvaluator.resolvePermissionSets reads metadata, then the bootstrap list, then the dbLoader the security plugin passes. That loader is catalogSetLoader (security-plugin.ts around line 1984), which calls securityCatalogReaderOf(ql).resolve('permission', name), the registry, not sys_permission_set.

So what S5c still owed was two comments in packages/verify/src/rls.ts that described the pre-ADR-0131 model as current:

  1. rlsProbeSecurity's docblock said the evaluator falls back to "the DB row only as a last resort", and that this loader drops rowLevelSecurity. Neither holds. The corrected text says why the probe set must be registered: a row-only set resolves nowhere and grants nothing.
  2. The inline comment in provisionRlsProbePersona said "The grant is keyed by ROW ID, and resolveUserAuthzGrants reads the row only to learn its NAME". The corrected text says the grant holds its set by name. The set row is read only for the id that the still-required permission_set_id column takes. The platform derives the grant's name from that id and refuses a disagreement (grant-permission-set-name.ts).

The code is unchanged. Retiring the set-row read and mint is #15204 stage 4, which is its own PR.

Changeset

Comments only, with no behaviour change, so this takes skip-changeset under the repo's comment fast lane.

Verification

All at 740affb:

  • pnpm --filter '@objectstack/verify...' build: exit 0.
  • pnpm --filter @objectstack/verify test: 29 files, 234 tests passed.
  • pnpm --filter @objectstack/verify typecheck: tsc --noEmit clean, and check:test-typecheck OK.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE: 47 derived, 46 run, 1 NOT MEASURED.
    • The NOT MEASURED gate is check:dual-build-cjs-loads, which exited 3 (PREREQUISITE NOT MET). It needs a whole-repo pnpm build, which is left to CI.
    • On their first run, check-plugin-teardown-shape --self-test (shallow clone) and check:dts-closure (dist trees not yet built) refused their prerequisites. After git fetch --unshallow and the build, both re-ran at exit 0.

Acceptance notes


Generated by Claude Code

Two comments in rls.ts still described the grant resolution that ADR-0131
D3/D4 replaced: that the grant is keyed by row id and the resolver reads the
set row to learn its name, and that resolvePermissionSets falls back to a
sys_permission_set row that drops rowLevelSecurity. On main the resolver
reads the grant's permission_set column and every definition source is the
registry. The row is read only for the id the still-required
permission_set_id column takes.

Claude-Session: https://claude.ai/code/session_01M5baMwzrfukawVWc496yxD
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

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 680a86b4c55dcb6255151d9a038a32208400f66c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f9dbbc4eafcc73911f559b45ce151be3f269f671 — the merge of head 740affb09843c796f3e37eccabbef55215d146bc into base 680a86b4c55dcb6255151d9a038a32208400f66c, 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 f9dbbc4eafcc73911f559b45ce151be3f269f671 && git checkout f9dbbc4eafcc73911f559b45ce151be3f269f671
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 680a86b4c55dcb6255151d9a038a32208400f66c 740affb09843c796f3e37eccabbef55215d146bc && git checkout -B drift-repro 680a86b4c55dcb6255151d9a038a32208400f66c && git merge --no-ff 740affb09843c796f3e37eccabbef55215d146bc

node scripts/docs-audit/affected-docs.mjs --json 680a86b4c55dcb6255151d9a038a32208400f66c

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 08:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 08:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 14f4912 Oct 11, 2026
40 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-15196-s5c-verify-name-column branch October 11, 2026 09:06
marchtian pushed a commit that referenced this pull request Oct 11, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants