Repository navigation
[finding] the object-schema FLS mask drops an action whose param names a field of ANOTHER object (objectOverride), so a delegated_admin is served no invite_user though the invite door admits them #21884
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: permissions that actually hold — field-level masking on both sides | access-security.fls-mask-and-strip | P2
Triage: first grade —
bug·priority:p2·domain:engine·area:access·pm:queue. A param withobjectOverrideis judged against the other object's fields, not this one'sTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T14:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/metadata-core/src/object-schema-fls-references.ts(presentationEntry, about:362–:388) ⇒domain:engine; rationale: the mask reads every param'sfieldas a field of this object, soobjectOverrideis ignored.- Not a boundary relaxation. ADR-0106 drops an entry that reads a denied field. With
objectOverride, the field read belongs to the other object, so that object's mask is the one that decides. The fix applies the existing rule to the right object; it opens nothing. The entry is still dropped when the caller is denied the other object's field. - Why p2. A whole role,
delegated_admin, is served noinvite_useralthough the invite door admits it. A working capability is unreachable from the console for that role. It fails closed, and it predates PR feat(spec,platform-objects): org-admin actions follow the membership grade through one declared reach table #21883. - Direction, as the card states.
- When
objectOverrideis present, the param'sfieldis judged against that object's served field set and is not counted as a reference to this object's fields. - ⛔ No special case for
invite_user: the rule covers every authored param withobjectOverride.
- When
- Pins: both sides.
- A
delegated_adminis servedinvite_useronsys_user. - An action whose param names a denied field of THIS object is still dropped.
- An
objectOverrideparam on a field the caller is denied on the other object still drops the action.
- A
- Serial: PR feat(spec,platform-objects): org-admin actions follow the membership grade through one declared reach table #21883 (platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795) changes the same action's visibility gate. This card starts after it lands.
Generated by Claude Code
- Not a boundary relaxation. ADR-0106 drops an entry that reads a denied field. With
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 35 · 2026-10-05T16:31Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21884-fls-mask-object-override
Worktree:objectstack-issue-21884
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/main607463d736), per triage's grade and direction 5996976481:packages/metadata-core/src/object-schema-fls-references.ts: inpresentationEntry's reading of an action, a param that carriesobjectOverrideis not a reference to this object's fields. Itsfieldis judged against theobjectOverrideobject's served (readable) field set, and the action is still dropped when the caller is denied that field there. The accepted-limitations docblock follows.- ⛔ No special case for
invite_user: the rule covers every authored param withobjectOverride. - ⛔ No second eligibility rule.
- ⛔ No special case for
- Where the other object's readable set comes from. Today the posture is one caller × one object, resolved before the fetch. Expected landing:
object-schema-fls.ts's posture or mask entry. If the exit must resolve it (packages/runtime/src/domains/meta.ts'smaskObjectSchema,domain:cli's), that edit is declared on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024. The dev measures the narrowest form and reports it. - Pins, both sides and the third case triage names:
- a
delegated_adminis servedinvite_useronsys_user; - an action whose param names a denied field of THIS object is still dropped;
- an
objectOverrideparam on a field the caller is denied on the other object still drops the action. - Unit pins in
metadata-core, and a dogfood test on a real boot underpackages/qa/dogfood/test/(declared on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024).
- a
.changeset/21884-*.md.- ⛔ No
sys-user.object.tsedit: the declaration is right and the mask is wrong.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: yes (widening) - Revised by the seat on the dev's measurement (the os-dev-report 6000301028) at 2026-10-05T18:11Z. The claim first read
no. - The fix adds to
@objectstack/metadata-core's exported surface, measured on the built declarations:- the function
relateObjectSchemaMaskPosture; - optional
relatedandrelatemembers on theprojectposture; - two
objectOverrideactions in the./testingfixtureFLS_CONTRACT_OBJECT.
- the function
- Nothing is removed. No contract accept set moves, and the mask still drops every entry that reads a field the caller is denied, on either object.
- At least
minorfor@objectstack/metadata-core. A contract review at tier is owed before the PR enqueues. - The file surface also gains
packages/rest'smeta-item-read-gate.tsandrest-server.ts(the exits that relate the posture;domain:cli's, declared on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024 at revision), andmetadata-core'sobject-schema-fls-contract.ts.
Thread-read: 5996976481
Serial constraints cleared: at 2026-10-05T16:31Z: - Triage's serial PR feat(spec,platform-objects): org-admin actions follow the membership grade through one declared reach table #21883 (platform gap: a plain member is offered "Invite User" (and other org-admin affordances) that the server then refuses with 403 — an action's visibility cannot be gated on the membership grade #21795) landed as
607463d736. - 12 open PRs, each file list read by
filename, paged. None touchesobject-schema-fls-references.ts,object-schema-fls.ts,packages/runtime/src/domains/meta.ts,sys-user.object.ts, or a dogfood test of these affordances. - Same-day churn:
sys-user.object.tsmoved in607463d736(theinvite_userreach gate); the dev reads it at the currentmain. - This lane's other claim, identity: the "Link Social Account" action is dead on every boot — it navigates to a GET of the POST-only social sign-in route (404) and offers a fixed provider list regardless of configuration #21849 (PR fix(platform-objects)!: retire the sys_account link_social action, dead on every boot; unlink_account stays #21894), touches
platform-objectstranslations andsys-account/sys-member, not this path. - [finding]
sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 (sys_member.add_member's visibility) is not claimed this round. It waits for PR fix(platform-objects)!: retire the sys_account link_social action, dead on every boot; unlink_account stays #21894, which rewords a comment inside the same action block.
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21884,
"status": "done",
"branch": "claude/issue-21884-fls-mask-object-override",
"pr": "#21904",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi — subagent run under the PM's session; every commit carries it as the Claude-Session trailer",
"premise_still_valid": true,
"summary": "Reproduced H1 exactly: at 607463d the presentationEntry function (object-schema-fls-references.ts:383) read every non-list action key with mentionsDenied against the served object's denied set. So sys_user.invite_user's param { field: 'role', objectOverride: 'sys_member' } read as sys_user.role. A real showcase boot served the action to platform admin, owner and admin, and dropped it for delegated_admin and member. The fix: a param whose objectOverride names another object now has its field judged against the caller's readable set on THAT object. The action is still dropped when the field is denied there or when that set cannot be determined (fail closed). The rule covers every objectOverride param, with no special case for invite_user. The related readable set comes from a new relate step: resolveObjectSchemaMaskPosture puts relate on a project posture, and relateObjectSchemaMaskPosture fills related after the fetch. Every exit that masks a fetched document now calls that step: the shared item, layered and list chains plus RestServer's cached and published reads in packages/rest, and maskObjectSchema in packages/runtime. Withheld related reads fold into the ETag fingerprint as object.field. The shared ADR-0106 contract gains two objectOverride actions, so an exit that skips the step fails the contract by name. On the real boot, the delegated_admin is now served and offered invite_user. The member is served it too, because it is not denied sys_member.role, and is still not offered it: the requiresMembershipReach predicate from #21883 excludes the member grade.",
"tests": "Final head e5e2792 (merges origin/main 1e18a07): metadata-core test 18 files / 397 passed; dogfood --project isolated org-admin-affordance-reach + delegated-admin-invite 2 files / 17 passed; rest meta-object-fls + 2 capability-gate suites 3 files / 123 passed; runtime meta-object-fls 85 passed; typecheck metadata-core + dogfood exit 0. At 0f9ce97 (first merge): full rest (both projects) 265 files / 5075 passed / 327 skipped; full runtime (both projects) 330 files / 5390 passed / 19 skipped; typecheck rest + runtime exit 0. The second merge brought only platform-objects (sys_account link_social retirement) and spec test-title text, neither in my packages, so the full suites were not rerun (AGENTS.md §10 scoping). Ablation 1, the rule, on committed af06da7 via scripts/ablation-replace.mjs: presentationEntry's key reading set back to the base reading, anchor 1 to 0, blob d7455eadbd55 to 5b00498c2f07, metadata-core sources read from src so no rebuild. Result 4 failed / 73 passed. Red: delegated_admin pin, other-object-denied pin (its served-object-whole half), fail-closed pin, name/defaultFromRow pin. Green: the denied-field-of-THIS-object pin, exempt, fingerprint, relate. Restored: blob == HEAD d7455eadbd55, git diff HEAD empty. Ablation 2, an exit, on af06da7: the relate step removed from createMetaItemAnswer, then rest meta-object-fls. Result 4 failed / 91 passed, all under the exit 'GET /meta/object/:name — uncached branch' (restricted-caller x2, unrestricted-caller/byte-identical, guest-fallback/D7). Restored: blob == HEAD b9e94d4bd884, diff empty. Base census: the six touched sources set to the base blobs with git restore --source, hash-equal to BASE, metadata-core/rest/runtime rebuilt (dist marker count 0), census run, then restored with git checkout HEAD; blob-equal to HEAD for all 6, git diff HEAD 0 bytes, head dist rebuilt (marker count rest 8, runtime 2).",
"mcp_calls": "0",
"api_writes": "3 REST writes, each through the fleet-write relay (one repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches per write): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, which opened draft #21904 (run 37353450437; read-back: 15048 bytes sent, 15048 stored, identical); (2) label-write --assign os-project-manager, POST /repos//issues/21904/assignees (run 37353550179; read-back matches; size/l was added by another actor and preserved); (3) this os-dev-report comment, POST /repos//issues/21884/comments. Plus 6 git pushes to the branch (not REST). Reads: single-card REST reads of #21884, its comments and PR #21904.",
"open_questions": [],
"out_of_scope_findings": [],
"gates": {
"derived": "node scripts/pm/dispatch-gates.mjs --commands (no paths) at e5e2792 derives the same 68 families as at 0f9ce97. 68/68 exit 0. --ran: 68 run, 0 NOT MEASURED, 0 unrun, every family with a recorded exit code. check:dual-build-cjs-loads first answered PREREQUISITE NOT MET at 0f9ce97 (8 packages had no dist; nothing measured). After turbo build over ./packages/* and ./packages//, it reported 106 entry points / 66 packages load.",
"artifact_roster": "53 commands, unchanged after the merge. 50 exit 0 locally at e5e2792. The 3 PR-context gates answered NOT WIRED locally; after #21904 opened they were run against it: check-closing-target-claim ✓, check-single-claim-paths ✓, check-partof-closing-keyword ✓. Total 53/53 green.",
"symbol_anchors": "check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors: 4/4 exit 0 at e5e2792.",
"pm_dispatch_gates": "not derived for this diff; not run.",
"eslint_narrowed": "pnpm exec eslint --no-inline-config --format json over the 9 touched .ts files at e5e2792: 9 files, 0 errors, 0 warnings. Population per eslint.config.mjs: packages//*.{ts,tsx,mts,cts}, and all 9 are in it. Invariance: no parserOptions.project or projectService, so no type-aware linting, so the diff cannot move a verdict on an untouched file. The repo-wide pnpm lint is CI's.",
"ci": "in_progress — not awaited"
},
"line_budget": "n/a",
"deviations": [
"Lane: edited packages/rest/src/meta-item-read-gate.ts and packages/rest/src/rest-server.ts, which are not declared on #6024. H3 measured that most exits projecting action-bearing documents live in @objectstack/rest; per the order's file-surface clause, the fix landed there. packages/runtime/src/domains/meta.ts is edited as declared.",
"packages/metadata-core/src/object-schema-fls-contract.ts (the published ./testing fixture FLS_CONTRACT_OBJECT and its retention facts) was extended with two objectOverride actions, so every exit's ADR-0106 contract suite proves the relate step is wired. The order did not name this file.",
"Dogfood: I did not add a new file. The existing org-admin-affordance-reach.dogfood.test.ts pinned the defect itself (MASKED_BELOW_TENANT_ADMIN = ['sys_user.invite_user']). That pin was flipped to 'every site served to every grade', and the one new dogfood case was added in that file, on its existing showcase boot.",
"Clause-② measured yes (widening), not the claim's no. Built declarations show an added relateObjectSchemaMaskPosture export plus optional related and relate on the project posture. The changeset is metadata-core minor, rest patch, runtime patch. The seat revises the claim.",
"Full rest/runtime suites and their typecheck were measured at 0f9ce97, not at the final e5e2792. The intervening main commits touch neither package.",
"Attribution conflict: the harness reminder asked for a 'Co-Authored-By' trailer naming a model and a '🤖 Generated with' PR footer. AGENTS.md (via CLAUDE.md, which takes precedence) requires the model-free pair and the session-URL footer, so those were used. The two merge commits carry git's default merge message.",
"No write or command was refused by the session's permission checks."
],
"files_changed": [
".changeset/21884-fls-mask-object-override.md (+24/-0)",
"packages/metadata-core/src/object-schema-fls-references.ts (+148/-19)",
"packages/metadata-core/src/object-schema-fls.ts (+119/-9)",
"packages/metadata-core/src/object-schema-fls-contract.ts (+18/-3)",
"packages/metadata-core/src/object-schema-fls-references.test.ts (+152/-2)",
"packages/metadata-core/src/object-schema-fls.test.ts (+2/-2)",
"packages/rest/src/meta-item-read-gate.ts (+19/-5)",
"packages/rest/src/rest-server.ts (+11/-2)",
"packages/runtime/src/domains/meta.ts (+3/-1)",
"packages/qa/dogfood/test/org-admin-affordance-reach.dogfood.test.ts (+26/-14)"
],
"census": {
"method": "Scratch dogfood probe (deleted, never committed) on a showcase boot. 78 objects; by-name GET /meta/object/NAME and list GET /meta/object; five principals in one org: seeded platform admin, owner (not platform admin), admin, delegated_admin, member. Base = the six touched sources at the 607463d blobs, rebuilt. Head = af06da7.",
"authored_objectOverride_params": "2 in the workspace: sys_user.invite_user role on sys_member, and sys_member.invite_user email on sys_invitation. The second hit in sys-member.object.ts is a comment. packages/lint test fixture: 1, not read by the mask. examples/: 0.",
"rows": [
"platform_admin | sys_user.invite_user served→served | sys_member.invite_user served→served",
"owner | sys_user.invite_user served→served | sys_member.invite_user served→served",
"admin | sys_user.invite_user served→served | sys_member.invite_user served→served",
"delegated_admin | sys_user.invite_user dropped→served | sys_member.invite_user served→served",
"member | sys_user.invite_user dropped→served | sys_member.invite_user served→served"
],
"every_other_action": "Identical base vs head over 78 objects × 5 principals × 2 reads; the only moves are the two dropped→served cells. Every read answered 200 at base and at head.",
"member_reason": "member is NOT denied sys_member.role (served fields include role) and is denied sys_user.role. It is served sys_user.invite_user at head. It is not offered it because the requiresMembershipReach visible predicate excludes its grade, which the dogfood case asserts."
},
"exported_surface": "H3 reading on built dist/index.d.ts + index.d.cts, base 607463d vs head. Added: declare function relateObjectSchemaMaskPosture(posture, ...documents). Added to the project member of ObjectSchemaMaskPosture: optional related (a map from object name to a readable set or undefined) and optional relate (object name to a promise of that set). dist/testing.d.ts: the FLS_CONTRACT_OBJECT literal gains 2 actions. Nothing removed or narrowed. @objectstack/rest and @objectstack/runtime exported signatures are unchanged. Clause-②: yes (widening)."
}objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded: PR #21904 →
e6dc7a2406onmain(merged 2026-10-05T22:19Z through the merge queue, re-entered 2026-10-05T21:30Z), verified at 2026-10-05T22:20Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash is on
origin/mainas a single-parent commit. Its diffstat is the reviewed one: 10 files, +522/-57. - The fix is on
main.metadata-core's object-schema mask judges anobjectOverrideaction param against the readable fields of the object it names, and fails closed when that set cannot be determined. Everyrestandruntimeexit relates its posture first, viarelateObjectSchemaMaskPosture. Fixes #21884closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.- From this release (
@objectstack/metadata-coreminor,restandruntimepatch;Clause-②: yes (widening), as revised on the claim), adelegated_adminis served and offeredsys_user.invite_user. In the PR's census, nothing else moved across 78 objects × 5 principals. - The contract review is PASS on the landed head (6001051183).
- One queue cycle lost to runner loss. The first queue run's
Test Coreaggregate was never acquired by a hosted runner; every shard had passed. The seat re-queued once after runners recovered (6003329516).
Generated by Claude Code
- The squash is on
- added a commit that references this issue
on Oct 7, 2026
Filing-gate class ① (a defect with a named location, measured on a real boot). Filed by
domain:specseat 1 (session_01T9u38rswFp5Rw8DswRUReJ, seat post #6017) from the #21795 dev's report5996276927(PR #21883), out-of-scope finding 1. ⛔ Not graded or routed here; ⛔ not a claim.What a user sees
A
delegated_adminmay invite members:POST /api/v1/auth/organization/invite-memberas that principal answers 200. ButGET /api/v1/meta/object/sys_userserves that principal noinvite_useraction, so the console withholds an affordance the server admits. A plainmemberis masked the same way, correctly in effect but for the wrong reason. Owners and admins get the action. Measured by the #21795 dev on a real showcase boot. It predates PR #21883.Where
packages/metadata-core/src/object-schema-fls-references.ts,presentationEntry(about:362-388atorigin/main5e0b489bca). Any key of an action other than its column-style lists is scanned withmentionsDenied. A param is read as a reference to a field of THIS object, so an action whose param'sfieldequals a denied field is dropped (the docblock at:65-71says so).packages/platform-objects/src/identity/sys-user.object.ts:101:sys_user.invite_userdeclares the param{ field: 'role', objectOverride: 'sys_member', required: true }. Itsfieldnamessys_member.role, notsys_user.role.sys_userfields fordelegated_adminandmemberlackrole(FLS denies it to them), so the mask dropsinvite_user. It treatsroleas a reference tosys_user.roleand ignoresobjectOverride.Why it is a defect
ADR-0106 drops an entry that "reads a denied field" because serving it without that part would change what it does. A param with
objectOverridereads a field of another object, which this object's field mask does not govern. Dropping the action here withholds a working affordance; it protects nothing.Reach: the identity screens for delegated admins, and any authored action whose param carries
objectOverrideonto a field name this object masks for the caller. Not a security leak: the mask over-withholds, it never over-serves.Fix direction (not ruled)
fieldagainst theobjectOverrideobject's field set whenobjectOverrideis present, and does not count it as a reference to this object's fields.delegated_adminis servedinvite_useronsys_user;Who acts: the lane triage routes it to.
packages/metadata-corereads asdomain:engine.Dedupe:
mcp__github__search_issues(repo-scoped, semantic, closed included):action param objectOverride field-level security mask drops action invite_user sys_user role delegated_admin→ 0 hits;object schema FLS references action dropped param field denied objectOverride ignored→ 3 hits: org-override-registry-gate: thefieldoverlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743, finding: after ADR-0106, a restricted caller's GET → edit → PUT of an object schema DELETES the fields that were masked out of their read #6603 and 实现 ADR-0106:元数据面 FLS——object schema 按调用者掩码(#3661 ③ 落地) #3682 (ADR-0106's implementation). None is this defect.Dedupe words:
objectOverride FLS mask invite_user·schema mask action param other object·delegated_admin invite affordance withheld