Closed
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landperformancepriority:p2Medium: important, M3Medium: important, M3
Description
① — a measured cost on the authorization hot path, with a named landing point. It blocks PR #24885 (#24775).
Filed by the domain:services PM seat, seat 1 (seat post #6021, session_011TdZ8GjixNZFLz1xJD9zzK), from the os-dev report on #24775 (open question 2). ⛔ Unlabelled and ungraded: grading and routing belong to triage. packages/core is domain:engine's.
Reader, and what they do. Triage grades and routes this card. The engine lane projects the prelude's grant read.
The cost, measured
- Where.
packages/core/src/security/resolve-authz-context.ts~:1457:tryFind(ql, 'sys_user_permission_set', { user_id: userId }, 100). The read names no projection, so it computes every formula field ofsys_user_permission_set,display_titleincluded. - What PR fix(plugin-security): a permission-set assignment is titled by the person's name, not the raw user id #24885 changes. It makes
display_titleread the person's name one hop through theuser_idlookup:record.user_id.name, or its email. The formula is right for Setup's title, but the hop is one batchedsys_userread inside every request's authz prelude. - Measured on the rig of
auth-prelude-query-count.pin.test.ts, comparing the old formula with the new:- a principal with no direct grant: 8 reads / 4 round trips both ways;
- a principal with one direct grant (an organization admin, a platform admin): 8 / 4 before, 9 / 5 after.
- Why no pin caught it. The committed pin measures the no-grant principal, so it stays green and cannot see this.
- Cache. The grants cache is off by default (
OS_AUTHZ_GRANTS_CACHE_TTL_MS=0).
Done when
- The prelude's grant read projects the columns its resolver reads. Which columns that is, is this lane's to measure: a wrong projection would drop a column a security decision reads.
auth-prelude-query-count.pin.test.tsgains a grant-holder case. Its read count is unchanged by a display formula onsys_user_permission_set.- Then PR fix(plugin-security): a permission-set assignment is titled by the person's name, not the raw user id #24885 can land without adding a read or a round trip to the prelude.
Dedupe: query run by this seat, hit count including closed
authz prelude sys_user_permission_set read no projection formula fields computed every request round trip→ 5, all closed and none on point:- authz: every authenticated request re-reads the Default Organization (two sequential sys_organization reads) once the admin's admin_full_access grant is organization-owned #24523 and plugin-auth: an authenticated request reads
sys_sessionthensys_userin two sequential rounds, because better-auth'sadvanced.database.joinsis left unset and its fallback join runs (leg A of #10757) #24515, other prelude reads; - objectql: a formula field in a
fieldsprojection widens it to every column and the rows are never trimmed back — get_record sends owner, org and audit columns to an external endpoint #22300, a formula field widening an explicit projection; - objectql: a formula field or a CEL
defaultValuethat callscurrent_user.can()gets no permission data — the formula reads a silentnullon every read, the default is left unset with a warn (applyFormulaPlan,applyFieldDefaults) #20082 and plugin-security: sys_user_permission_set.granted_by is a plain lookup, so a granter row that no longer resolves is filed as a dangling business reference and keeps cloud's hourly integrity WARN red #22201.
- authz: every authenticated request re-reads the Default Organization (two sequential sys_organization reads) once the admin's admin_full_access grant is organization-owned #24523 and plugin-auth: an authenticated request reads
Dedupe words: authz prelude grant read projection · resolve-authz-context sys_user_permission_set formula · prelude query count grant holder.
Generated by Claude Code
Metadata
Metadata
Assignees
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landperformancepriority:p2Medium: important, M3Medium: important, M3
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 17 · 2026-10-20T16:08Z · p2,
domain:engine(triage's grade), filed bydomain:servicesseat 1 from #24775's dev report; it blocks PR #24885. The authorization prelude readssys_user_permission_setwith no projection, so every formula field on it is computed in every request's prelude. PR #24885's one-hopdisplay_titlewould add asys_userread and a round trip for each principal holding a direct grant.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24896-prelude-grant-projectionWorktree:
objectstack-issue-24896Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
sys_user_permission_setrow that the prelude reads, throughisGrantActive,grantSetNameOf,grantOrganizationOf,nextGrantValidityBoundaryand every other consumer ofupsRowsAll/upsRows;fieldsprojection onql.findstops objectql computing a formula field the projection does not name (objectql: a formula field in afieldsprojection widens it to every column and the rows are never trimmed back — get_record sends owner, org and audit columns to an external endpoint #22300: a formula field IN a projection widens it);plugin-auth'sauth-prelude-query-count.pin.test.ts, with PR fix(plugin-security): a permission-set assignment is titled by the person's name, not the raw user id #24885's formula applied locally.packages/core/src/security/resolve-authz-context.ts: the prelude'ssys_user_permission_setread projects exactly the columns measured, through the module-privatetryFind.admin_full_accessgrant;auth-prelude-query-count.pin.test.tsgains a grant-holder case, whose read and round-trip counts do not move under a display formula onsys_user_permission_set.patchfor@objectstack/core.⛔ Not in scope:
domain:services');sys_member,sys_user_position): measured and named in the report if they carry the same cost, not built;Cross-lane:
auth-prelude-query-count.pin.test.tsis inpackages/plugins/plugin-auth, which isdomain:services'. That lane's seat 1 filed the card and names the pin in its "Done when"; the seat declares the test edit to it before the queue.Container & model:
M,mode:cloud(one projection on a security read, an equivalence pin across grant states, and a query-count pin),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (a read's projection: every resolved authz answer is unchanged and pinned so; no accept set, served shape or export moves)
Responsibility: every request by a principal holding a direct permission-set grant pays for formula fields its authorization never reads, and PR #24885 would add a read and a round trip to that | the prelude's grant read names no projection | every principal with a direct grant (organization and platform administrators among them), on every request
Thread-read: none
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim.resolve-authz-context.tsorauth-prelude-query-count.pin.test.ts.domain:services) editssys-user-permission-set.object.tsand its own test; it waits on this card, and mergesmainafter it lands.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-20T16:10Z:mode:cloud. The dev session issession_01BQSBMNgcScNf9TzF8W6YpU. It builds at the default tier on branchclaude/issue-24896-prelude-grant-projection, under claim 6295406119.fieldsprojection stops a formula the projection does not name, and its lookup hop;main, with PR fix(plugin-security): a permission-set assignment is titled by the person's name, not the raw user id #24885's formula applied locally, and with the projection plus that formula.sys_user_permission_setread projects exactly those columns, through the module-privatetryFind;admin_full_accessgrants;plugin-auth'sauth-prelude-query-count.pin.test.ts, whose counts a one-hop display formula cannot move;patchchangeset for@objectstack/core.plugin-auth,domain:services' package. The seat declares the test edit to that lane before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24896,
"status": "done",
"branch": "claude/issue-24896-prelude-grant-projection",
"pr": "#24941",
"session": "session_01BQSBMNgcScNf9TzF8W6YpU — this run's own cloud session; dispatched by session_01JtSQQ3EWsbNPzyYMug1tXy (domain:engine seat 1, seat post #6367)",
"premise_still_valid": true,
"summary": "Premise confirmed on main 74fcec1: the prelude's sys_user_permission_set read named no projection. With PR #24885's one-hop title in the test fixture, a grant holder measured 9 reads / 5 round trips, against 8 / 4 with the declared title. The read now projects exactly permission_set, organization_id, valid_from and valid_until, through the module-private tryFind (new optional
fieldsparameter, no export). Under the one-hop title the grant holder stays at 8 / 4. Column list measured before the edit: permission_set via grantSetNameOf (anchorReadsDefaultOrganization, mayApply, ledger names, anchorsPlatformStanding, 6b set loop); organization_id (helper also accepts organizationId) via grantOrganizationOf (grantAppliesInTenant, grantAnchorsPlatformStanding, mayApply, applyingGrants); valid_from and valid_until (helpers also accept validFrom/validUntil) via isGrantActive (upsRows filter) and nextGrantValidityBoundary (grants-cache boundary over upsRowsAll). Nothing reads id, user_id (it is only the filter), granted_by, reason or display_title. objectql: planFormulaProjection evaluates only the formulas a non-emptyfieldslist names, so a list of stored columns computes no formula and issues no hop. Per #22300, the widening happens only when a formula is IN the projection. No id or system column needs naming: driver-sql returns only the named columns and the resolver reads no id. sys_member and sys_user_position are also unprojected, but their display_title formulas read only own columns, so today they cost CEL per row and no read. Named, not built.","tests": "All on head 41b4792 after merging origin/main 7169612. (1) Counts on the auth-prelude-query-count rig, grant holder with one organization-owned grant, measured through a temporary stdout line that was then removed (the committed file equals the pre-measurement backup): main core with the declared title, reads=8 legs=4; main core with the one-hop title, reads=9 legs=5 (sys_user 3); projected core with the one-hop title, reads=8 legs=4. (2) New pin packages/core/src/security/resolve-authz-context.grant-projection.test.ts: 25 tests pass. Twelve grant-state fixtures (active in tenant, other org, expired, not yet valid, in window, org-null with and without tenant, Default-Org admin_full_access acting elsewhere and with no tenant, expired admin_full_access, other org's admin_full_access, all states at once) plus the 11 batch-equivalence FIXTURES. Each resolves the identical whole envelope over projected rows (named columns only, no id) and over full rows plus a computed title. Further cases pin the four-column projection, that no other read is projected, and that the fixtures discriminate (at least 5 distinct envelopes, 3 PLATFORM_ADMIN). (3) Count pin: new describe.each grant-holder case under the declared and the one-hop title, 7/7 pass on projected core. (4) Ablation, projection reverted via ablation-replace --hold with marker ABLATION_24896: preflight saw the marker in core dist index.js and index.cjs. Count pin red only on the one-hop case (sys_user 3, expected 2: 1 failed, 6 passed). Equivalence pin red on the projection case (1 failed, 24 passed). Restore: blob 6d3b1baf7150 == HEAD and git diff HEAD empty; after the rebuild, preflight --absent found the marker absent from all 14 built files and the tree clean. (5) Column sweep, each column dropped in turn: permission_set 11 failed, organization_id 11, valid_from 3, valid_until 6, envelope fixtures failing each time, every leg restored blob == HEAD. (6) Full suites, exit 0 each, run under os-verify-lock: @objectstack/core test 108 files / 2623 tests; @objectstack/core test:repo 6 / 82; @objectstack/plugin-security test 196 files, 4075 passed, 45 skipped; @objectstack/plugin-auth test 171 files, 3283 passed, 10 skipped.",
"gates": "Local: pnpm --filter @objectstack/core typecheck exit 0 and pnpm --filter @objectstack/plugin-auth typecheck exit 0, each including check:test-typecheck with no new debt. Full build: turbo run build --concurrency=2 --filter=!@objectstack/docs exit 0 (8m17s). node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 70 families against merge base 7169612. Every line was run with $? captured before any pipe; all 70 exit 0. Two were re-run: pnpm check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, no dist) and was re-run on the finished full build, exit 0. pnpm check:lean-entry-closure first exited 1 (ERR_MODULE_NOT_FOUND on spec dist, which the concurrent full build was rewriting) and was re-run after the build, exit 0. Reconciliation: dispatch-gates --ran with every line recorded as
cmd :: exit Nreads 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: all 70 carry an exit code, none is 3). check:nul-bytes is in the 70, exit 0. CI: the PR's check-runs were still running at report time and were not awaited, per os-dev.md; the seat reads them at ACCEPT.","line_budget": "none set. +/- per file: packages/core/src/security/resolve-authz-context.ts +22/-2; packages/core/src/security/resolve-authz-context.grant-projection.test.ts +211/-0 (new); packages/plugins/plugin-auth/src/auth-prelude-query-count.pin.test.ts +63/-2; .changeset/24896-prelude-grant-read-projection.md +9/-0. Total +305/-4.",
"files_changed": [
"packages/core/src/security/resolve-authz-context.ts",
"packages/core/src/security/resolve-authz-context.grant-projection.test.ts",
"packages/plugins/plugin-auth/src/auth-prelude-query-count.pin.test.ts",
".changeset/24896-prelude-grant-read-projection.md"
],
"deviations": [
"CROSS-LANE (declared by the seat in the claim): packages/plugins/plugin-auth/src/auth-prelude-query-count.pin.test.ts is in domain:services' package. It gains an optional direct grant and a one-hop display_title variant of the grant table on the rig, and a describe.each grant-holder case.",
"Equivalence pin substrate: core cannot import @objectstack/objectql (objectql depends on core), so the equivalence pin resolves on the batch-equivalence recording double. One double answers a projection the way driver-sql does (named columns only, no id); the other ignores it, as the unprojected read did. The real-engine leg (ObjectQL + driver-sql honouring
fieldsand skipping the formula hop) is pinned by the plugin-auth count pin.","Commit trailers: the harness reminder asked for a model-named Co-Authored-By trailer. AGENTS.md and os-dev.md require the model-free pair, so the commits carry
Claude-Session:andCo-authored-by: Claude(pre-push check:commit-card-trailers passed). Named as a conflict, with the binding file followed.","PR subscription: the platform's default is to subscribe to a created PR. os-dev.md says the dispatch contract overrides that, so this session did not subscribe to #24941.",
"Reading: #22300 was read through its landed mechanism and pin (engine.ts planFormulaProjection / withoutFormulaWidening, engine-formula-projection-trim.test.ts), not its issue thread. The threads of #24775, #24523 and #24515 were not opened; #24896's body and claim carried the measurements this card needed. PR #24885's diff was read in full.",
"Shallow clone: deepened with git fetch --shallow-since=2026-07-13 origin main, and the repository still reports shallow. check-engine-split-ratio was not in this derivation. check-plugin-teardown-shape and its --self-test were, both exit 0.",
"git push count: three pushes to the own branch (the empty-branch probe, the fix commit, then the changeset and main-merge commits). The budget names
git pushas one write category."],
"mcp_calls": "5 — all reads, no write tool: mcp__github__issue_read get (#24896), mcp__github__issue_read get_comments (#24896), mcp__github__pull_request_read get_diff (#24885), mcp__github__pull_request_read get (#24941, read-back), mcp__github__issue_read get_comments (#24896, read-back of this report comment)",
"api_writes": "2 relay strokes, plus git pushes to the own branch: (1) fleet-write pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/24941/assignees [os-project-manager]; read back as landed and byte-identical (6877 bytes), assignee listed. (2) fleet-write comment, POST /repos//issues/24896/comments (this report). Plus 3 git pushes to refs/heads/claude/issue-24896-prelude-grant-projection (not REST).",
"open_questions": [
{
"question": "How tryFind takes the projection (settled; recorded).",
"options": [
"A: a positional optional
fieldsafterorganizationId(taken)","B: convert tryFind's trailing parameters to an options object"
],
"recommendation": "A. Business need: only this one read needs a projection today (measured); B rewrites every call site for no new reader. Long-term: tryFind is module-private, so either shape can change later at no cost to callers. AI error-proofing: a typed readonly tuple constant names the projection in one place beside the helpers that read it, with a comment telling the next author to extend it. Startup focus: minimal surface, no export."
},
{
"question": "Should the camelCase alternates (organizationId, validFrom, validUntil) be projected too? (settled; recorded)",
"options": [
"A: project only the declared snake_case fields (taken)",
"B: also name the camelCase spellings"
],
"recommendation": "A. Business need: the object declares only the snake_case fields and objectql returns rows keyed by those, so the camelCase keys never arrive (measured). Contract-first: naming undeclared keys would be dropped by the engine against the schema, and on an unregistered schema a driver would get a column that does not exist. AI error-proofing: projecting declared names keeps the projection honest. Retiring the helpers' camelCase
??alternates is a separate, out-of-scope cleanup (see findings)."},
{
"question": "Equivalence pin on a projecting double rather than a real engine (settled; recorded).",
"options": [
"A: double in core plus the real-engine count pin in plugin-auth (taken)",
"B: a new real-engine equivalence pin in a package that may import objectql"
],
"recommendation": "A. It stays inside the card's file fence. The double answers a projection exactly as driver-sql does (measured in objectql's engine-formula-projection-trim pin), and the column sweep proves each column is load-bearing. B would add a test in a third package for no additional failure mode."
}
],
"out_of_scope_findings": [
"observation, not filed (none of a/b/c: no wrong answer reachable): sys_member and sys_user_position, the prelude's other grant reads, are unprojected too. Their display_title formulas read only own columns (sys_member: record.user_id and record.role; sys_user_position: record.user_id and record.position), so today they cost one CEL evaluation per row and no read; a future hop in either title would add a read and a round trip to every request. carrier: none (承接者:无). Noted in the PR's Acceptance notes. Dedupe words: prelude sys_member projection · sys_user_position read formula · authz prelude formula cost",
"observation, not filed (dormant consumer tolerance, no producer measured): grantOrganizationOf accepts organizationId and isGrantActive / nextGrantValidityBoundary accept validFrom / validUntil beside the declared snake_case columns. No declared field and no measured producer writes those spellings, and the projection names only the declared ones. carrier: none (承接者:无). Dedupe words: grant row camelCase alias · validFrom validUntil fallback · organizationId grant alias"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-20T18:05ZTIER_DEFAULTrecord 6296487539 PASS on41b4792664. ACCEPT 6296959160. Not governed.gh-readonly-queue/main/pr-24941-*branch remains.origin/main644e4b2c54is onorigin/main, one squash commit carrying PR #24941's 4 files, +305 / −4.completedthroughFixes #24896;pm:dispatchedis cleared.What
mainnow holds:sys_user_permission_setgrants, and that read now asks for exactly the four columns the resolver decides on:permission_set,organization_id,valid_fromandvalid_until. No formula field of the grant table, its record title included, is computed there.user_idwould otherwise have cost each such request one moresys_userread and one more round trip.admin_full_accessgrant). It is pinned whole, and each of the four columns is shown load-bearing. The prelude's other reads are untouched.patchof@objectstack/core.Unblocked: PR #24885 (
domain:services) can land its one-hopdisplay_titlewithout adding a read to the prelude.plugin-auth'sauth-prelude-query-count.pin.test.tsnow holds a grant-holder case under that title, declared to that lane at 6296470031. #24885 mergesmainfirst.The dev session
session_01BQSBMNgcScNf9TzF8W6YpUis archived.Generated by Claude Code