Repository navigation
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
Description
Activity
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsTriage: first grade,
priority:p2·domain:services·area:access·pm:queue. Direction:granted_byis provenance; the lane settles the writer question (the dev recommends A)Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T07:02Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugins/plugin-security(sys-user-permission-set.object.ts,sys-user-position.object.ts,delegated-admin-gate.ts) andplugin-sharing(sys-record-share.object.ts) ⇒domain:services; rationale: plugin-security and plugin-sharing are that lane's (lanes/services.md:10). The audit inpackages/objectqlis not changed.- Why p2: one row keeps cloud's production integrity WARN red every hour (objectstack-ai/cloud#2274). A standing red alarm hides the next real finding.
- The A / B / C question is the lane's, not a decision card.
- This repo already calls the column provenance (
auto-org-admin-grant.ts:785), and ADR-0118 D1 says "an id or null, never a sentinel". - A makes "granted_by = the writer" enforced. B leaves a silent NULL path. C is a consumer-side carve-out.
- A tightens who can set the value and widens nothing. If the claiming seat reads A as moving a security boundary, it stops and asks the maintainer.
- This repo already calls the column provenance (
- Family: measure
sys_user_position.granted_byandsys_record_share.granted_byone by one, as the card says. - ⛔ The production row's sentinel is out of scope; nulling it is cloud's maintainer's write.
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardspriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 8, 2026 objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-08T07:16Z
Session:session_01WkL6Eijt432S1Y7ekb6ovQ
Account:os-bill(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-22201-granted-by-provenance
Worktree:objectstack-issue-22201
Domain:domain:services
Seat:domain:services#1(seat post #6021)
Seat ruling on the lane's A / B / C (triage6054446764): A.granted_byisreadonly, and the delegated-admin gate always stamps the writer on a non-system insert.The four axes:
- Business need: cloud's production integrity WARN, measured, plus ADR-0118 D1's "an id or null, never a sentinel".
- Long-term: "granted_by = the writer" becomes enforced, not hoped for. C is a consumer-side carve-out, and B leaves a silent NULL path.
- AI-error prevention: a caller, an AI-written app included, can no longer forge a granter.
- Startup focus: two packages, no new gate.
A tightens who can set the value and widens nothing, so it moves no security boundary outward. The maintainer can veto it.
File surface at
origin/main3b493184:plugin-security/src/objects/sys-user-permission-set.object.ts:granted_by(near:84) gainsreadonly: true.plugin-security/src/delegated-admin-gate.ts: thegranted_bystamps (near:645,:781,:820). On a non-system insert, the gate stamps the writer whatever the caller sent.- Family, measured one by one, each changed only if its writers measure as the same class:
plugin-security/src/objects/sys-user-position.object.ts,granted_by(near:106);plugin-sharing/src/objects/sys-record-share.object.ts,granted_by(near:230), with that object's own writer stamp if it has one.
- Read only, not edited: the system-context writers keep their value. These are
invitation-placement.ts:204,auto-org-admin-grant.ts:790andbootstrap-platform-admin.ts:1138. The last is in PR fix(plugin-security): the boot heal converges on duplicated permission-set names, and a refused existence read never inserts #22214, which is in the merge queue, in another region. - Tests and changesets: tests in the touched packages, plus one changeset per touched package.
- The pins: a row whose
granted_bynames a deleted user lands inprovenance, and the WARN does not fire. A non-system caller's explicitgranted_bylands as the writer. System writers keep theirs. - The control: a business lookup that resolves to nothing still lands in
dangling.
- The pins: a row whose
Exclusions:
- ⛔ No change to
packages/objectql's integrity audit. - ⛔ No
packages/spec. - ⛔ No production data write (the sentinel row is cloud's maintainer's).
Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier, a security-adjacent write path)
Clause-②: no (narrowing)- A non-system caller's explicit
granted_byis no longer stored as sent. That narrows a published data-door outcome, so the change is declared breaking. No surface is widened.
Responsibility:this repository's own metadata and gate: plugin-security declares sys_user_permission_set.granted_by a plain lookup and its delegated-admin gate keeps a caller-supplied granter | none: the integrity audit routes by readonly alone, and nothing stops a caller from naming any granter | cloud production's hourly integrity WARN (1 of 13 findings, objectstack-ai/cloud#2274), and every non-system grant write
Thread-read: 6054446764
Serial constraints cleared: of the 21 open PRs (each file list read), none touches the four files. No in-flight branch of feat(objectql,plugin-auth): the Default Organization is load-bearing undersingle; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195, feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196, feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207 or security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908 changes them.
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22201, "status": "done", "branch": "claude/issue-22201-granted-by-provenance", "pr": "https://github.com/objectstack-ai/objectstack/pull/22243", "session": "session_01WkL6Eijt432S1Y7ekb6ovQ", "premise_still_valid": true, "summary": "Ruling A is implemented and covers both tables. sys_user_permission_set.granted_by and sys_user_position.granted_by are now readonly: true. DelegatedAdminGate.assert now runs authorize (the old body) first. It then calls stampWriter, which sets granted_by = context.userId on every row of every admitted non-system insert into the two tables, whatever the payload carried. Tenant-level admins, delegates and self-delegations are all stamped. A refused write throws before the stamp. This replaces the three conditional stamps, which ran on the delegate paths only and only when the value was null. H1 holds. The audit routes on readonly===true alone (dangling-reference-audit.ts:470), and the WARN condition (:735) leaves provenance out. granted_by sits at :105 on origin/main 3b493184, not :84; the line drifted after 56bf27af. H2 measured on the real engine (ObjectQL + SqlDriver + SecurityPlugin), before -> readonly only -> after, the same on both tables: - tenant admin naming OTHER: OTHER -> OTHER -> writer; - tenant admin naming none: null -> null -> writer; - delegate naming OTHER: OTHER -> OTHER -> writer; - delegate naming none: writer -> writer -> writer; - system insert naming OTHER: OTHER -> OTHER -> OTHER; - invitation placement apply: issuer -> issuer -> issuer; - org-admin reconcile via a system sys_member write with attributedUserId: attributed -> attributed -> attributed. H2 also falsifies one mechanism premise. On insert, readonly alone strips nothing, because the engine create-side strip skips sys_ objects (staticReadonlyInsertSubject). The gate in-place middleware stamp is what lands, and #14088 is not involved. H4 measured: a non-system UPDATE renaming the granter (admin or delegate) landed as OTHER before. Now the update strip drops it and the row keeps its granter. H5 audit buckets (inspectDanglingReferences, per table): a granted_by naming an id with no sys_user row was dangling with readonly removed (ablation B), and is provenance with zero [integrity] warnings now. The control, a user_id naming nothing, stays dangling with exactly one warning. H3: sys_user_position is the same writer class and was changed. sys_record_share is a different class and was left unchanged: managedBy engine-owned, a single SYSTEM_CTX writer stamping context.userId ?? null, and no caller-supplied granter in GrantShareInput. Merged origin/main 0e9371f0c6 before opening the PR; every reading below is at b4ed57b0ad.", "tests": "All at HEAD b4ed57b0ad. TESTS: - plugin-security whole package (vitest run --maxWorkers=2, under os-verify-lock): Test Files 177 passed, Tests 3766 passed | 45 skipped, VERDICT command-exit 0. - plugin-security typecheck: VERDICT command-exit 0, check:test-typecheck \"0 file(s) / 0 error(s)\". tsc --listFiles over tsconfig.test.json lists all 3 touched test files. - Build: turbo build (no docs), 72/72 tasks successful. - New granted-by-writer-provenance.test.ts: 20 passed. delegated-admin-gate.test.ts: +16 cases. - write-preview-field-gate-parity.test.ts: the fixture stand-in gains granted_by, because the gate now stamps a tenant admin insert. It was 1 red at c52e189600 and is 24/24 green after. GATES: - dispatch-gates --commands with no paths derived 65 commands. 65/65 exit 0, plus spec check:generated exit 0 (\"All 15 generated artifacts are up to date\"). - --ran reconcile: \"65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN\", with an exit code recorded per command. - check-adr-0087-registration: \"1 declared-breaking changeset(s), each carrying an ADR-0087 disposition\", not-required (no-migration-prescription). - check-changeset-no-major: \"This diff introduces no `major` bump\". The level axis is NOT APPLICABLE locally (no PR payload). - check:engine-double-contract exit 0. check:i18n \"OK (9 package(s))\". check:dual-build-cjs-loads \"106 published require entry point(s) across 66 package(s) load\". check:nul-bytes OK. - At c52e189600, the earlier run had dual-build and i18n at PREREQUISITE NOT MET (exit 3). Both were re-measured green after a full build. - ESLint narrowed to the 6 changed .ts files with --no-inline-config: the json report lists 6 files, 0 errors, 0 warnings, none ignored. eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rules), so this diff cannot change the verdict on any untouched file. Repo-wide pnpm lint is left to CI. ABLATION: - Every leg used scripts/ablation-replace.mjs: anchor x1 -> x0, blob changed, restore proven blob==HEAD with git diff HEAD empty. No dist/ was in the path (relative source imports, objectql aliased to source), so the dist preflight does not apply. - A (stampWriter keeps a supplied value): 11 red, 92 green. Every \"names another user\" pin went red, e.g. Expected usr_admin, Received usr_other. Restore blob 8fb5c414ec38. - B1 (sys_user_permission_set granted_by readonly:false): 3 red (2 update pins Received usr_other; provenance pin: row in dangling), 17 green. Restore blob 2bed0a5ade90. The first B1 attempt was a no-op: the replacement was a substring of the anchor, the tool refused, and the command never ran. It was re-run. - B2 (sys_user_position readonly:false): 3 red, 17 green. Restore blob 9df5977a9179. - C (stampWriter call removed): 22 red, 81 green. Restore blob 8fb5c414ec38. NOT MEASURED (left to CI): Dogfood Regression Gate, Temporal Conformance, workspace-wide type-check lanes, Test Core shards. The 3 dogfood suites that read granted_by expect the writer or a truthy value, which this change keeps.", "mcp_calls": "0", "api_writes": "3 REST writes, all through the fleet relay (scripts/pm/fleet-write/dispatch.mjs; each stroke = one POST /repos/objectstack-ai/objectstack/dispatches 204, executed as objectstack-fleet[bot]). (1) pr_create POST /repos/objectstack-ai/objectstack/pulls -> #22243 draft, run 37750468639, read-back 9707 bytes identical. (2) label-write --issue 22243 --assign os-bill -> POST /repos/objectstack-ai/objectstack/issues/22243/assignees, run 37750534413, read-back matches. (3) this os-dev-report comment POST /repos/objectstack-ai/objectstack/issues/22201/comments. Git pushes (not REST): 6 to claude/issue-22201-granted-by-provenance (empty-branch probe, wip rig, fix, pins+changeset, fixture, merge of origin/main).", "files_changed": [ ".changeset/22201-granted-by-provenance.md", "packages/plugins/plugin-security/src/delegated-admin-gate.ts", "packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts", "packages/plugins/plugin-security/src/objects/sys-user-position.object.ts", "packages/plugins/plugin-security/src/granted-by-writer-provenance.test.ts", "packages/plugins/plugin-security/src/delegated-admin-gate.test.ts", "packages/plugins/plugin-security/src/write-preview-field-gate-parity.test.ts" ], "deviations": [ "Merged origin/main (0e9371f0c6, 7 commits including plugin-security seeder changes and the Changesets pre-mode entry) into the branch before pr_create, per AGENTS.md multi-agent section 10. The branch carries one merge commit, b4ed57b0ad.", "The first closure build with declarations was OOM-killed (spec DTS, exit 137, other worktrees building at the same time). The measurement ran on a JS-only closure (OS_SKIP_DTS=1, separate turbo hash). Full builds with declarations were done later for typecheck and the gates.", "bootstrap-platform-admin.ts is not called by the new pins (read-only; PR #22214 is in flight). Its path, a plain isSystem insert, is pinned by the generic system-insert case.", "Field descriptions were left unchanged to avoid re-keying four translated bundles. The sys_user_position description is still true but now incomplete; this is noted in Acceptance notes.", "The PR body footer uses the AGENTS.md session-URL form, and commits use the model-free trailer pair. The harness reminder spelled both differently; AGENTS.md takes precedence." ], "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted in the PR Family section, not filed. sys_record_share.granted_by (plugin-sharing) was left unchanged by the H3 rule (a different writer class). A granter there that no longer resolves is still filed under the audit dangling bucket. No production finding on that column was measured.", "carrier: 承接者:无 · noted in PR Acceptance notes, not filed. A non-system insert naming a granter is replaced silently: there is no droppedFields entry and no strip warning, because the create-side readonly strip skips sys_ objects. The writer is visible in the returned row." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsSeat ACCEPT: PR #22243 at
b4ed57b0· seatdomain:services#1(#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-08T08:37ZChecked against GitHub and the branch, not the report's prose (
os-dev-reporton this card).-
Form:
- The PR is a draft against
main. Its first line isFixes #22201, with a line-startClause-②: no (narrowing), the claim's line. The PR assignee isos-bill. - Seven files, all in
plugin-security: the gate, the two object files, three test files and one changeset. +481/−17. - No
packages/specpath and the arm isno, so no contract review is owed. The PR is not governed. - It merges clean with
main.
- The PR is a draft against
-
Diff, read by the seat:
DelegatedAdminGate.assertrunsauthorizefirst; a refused write throws and is never stamped. It then callsstampWriter, which setsgranted_byto the caller'suserIdon every row of every admitted non-system insert intosys_user_permission_setandsys_user_position, whatever the payload carried.- The three old conditional stamps are gone. They covered delegate paths only, and only when the value was null.
- Both
granted_byfields gainreadonly: true. - System writers never reach the gate, so they keep their value.
- This is ruling A as the claim (
6054701742) states it, and it widens nothing.
-
A premise falsified, accepted:
readonlyalone strips nothing on insert, because the create-side strip skipssys_objects. The gate's in-place stamp is what lands, andstripReadonlyFieldsusesObject.isto tell a hook write from a caller write, so a hook cannot clear a readonly field the caller also sent as null #14088 is not involved. On update, the strip drops a caller's value, as H4 measured. -
H3, family:
sys_user_positionmeasures as the same writer class and is changed.sys_record_shareis a different class: one system-context writer, and no caller-supplied granter in its input. It is left unchanged, as the claim's rule required.
-
Changeset, checked sentence by sentence:
- the
!title and the BREAKINGminorline, per this repo's launch-window convention; - the ADR-0087 disposition (
not-required, no migration prescription); - the three "What moves" items;
- every "Unchanged" item, including the system writers and
sys_record_share.
Each matches the diff.
check-adr-0087-registrationandcheck-changeset-no-majorare green. - the
-
Evidence:
- Before, readonly-only and after, on a real engine for every writer case, on both tables. A non-system named granter now lands as the writer; system and invitation writers keep theirs.
- The audit buckets: a vanished granter goes to
provenancewith zero warnings, and theuser_idcontrol staysdanglingwith one warning. - Ablations A, B1, B2 and C were each red, each restored to the HEAD blob.
- The package suite: 177 files, 3766 passed. Typecheck passes.
- 65/65 derived gates, plus spec
check:generated.
-
Carried, not filed (each in the PR's Acceptance notes or Family section):
- A non-system insert's named granter is replaced silently: there is no
droppedFieldsentry, because the create strip skipssys_objects. The writer shows in the returned row. sys_user_position.granted_by's description still says the gate stamps "for delegate writes". That is true but now incomplete; the dev left it rather than re-key four bundles. Carrier: the next edit of that field.sys_record_share.granted_by, a granter that no longer resolves, still lands indangling. No production finding names that column.
- A non-system insert's named granter is replaced silently: there is no
-
Landing to-do:
Lint & Repo GatesandTypeScript Type Checkmust besuccesson the current head, with every other check green or an expected skip.- Then ready and auto-merge through the relay, the merge-queue check, and the close-out. That close-out also tells the
repo:cloudseat on [PM seat] repo:cloud#1 — 🟢 hotlong · session_01WVbr5J6u8BHh8EyFtcWciH #6026 that the fix reaches cloud's alarm with its pin move.
-
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-08T09:45Z.PR #22243 merged through the merge queue as
ffb31fca. Onorigin/main:sys_user_permission_set.granted_byandsys_user_position.granted_byarereadonly;DelegatedAdminGatestamps the writer on every admitted non-system insert into both tables.
The integrity audit files a granter that no longer resolves under
provenance. The PR'sFixesline closed the cardcompleted. This note also removespm:dispatchedand the assignee.- Handed back to the
repo:cloudseat on [PM seat] repo:cloud#1 — 🟢 hotlong · session_01WVbr5J6u8BHh8EyFtcWciH #6026 in this act: the fix reaches cloud's hourly WARN with v18, when cloud's framework pin moves (decision: open v18 now and ship it in stages — release the last 17.x from main first without waiting for #21908's deny (A), skip the last 17.x (B), or keep #22009's order (C)? #22050, ruling B). The production row's sentinel stays cloud's maintainer's write, as the card says. - The seat's ruling A in the claim (
6054701742) stands, and the maintainer can veto it.
- added a commit that references this issue
on Oct 9, 2026
Filing gate: ① a product defect, reach measured. Cloud's production control plane raises the hourly
[integrity] stored references that resolve to nothing (#4551)WARN on every run. One of its findings issys_user_permission_set#ups_d85cfc63b091b7b1.granted_by→sys_user#manual:db-cutover-superadmin, 1 of 13 rows in the 2026-09-15 reading (objectstack-ai/cloud#2274, comment 5673661010).Found by the objectstack-ai/cloud#2274 dev (os-dev-report, objectstack-ai/cloud#2274 comment 6053481678, which carries the measurement and the draft below). Filed by the
repo:cloudseat (repo:cloud#1, R45, sessionsession_011jobP72PwN3whNm55GetXQ), because the fix lands in this repo. ⛔ Not a claim.The defect (read at
56bf27af, cloud's pin)packages/objectql/src/integrity/dangling-reference-audit.ts(about :470) routes a finding byreadonly === truealone. Areadonlyreference that resolves to nothing goes toprovenance, and the WARN condition (about :735) does not includeprovenance.packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts:84:granted_byisField.lookup('sys_user', { required: false, … }), notreadonly. So a granter that is gone, or a sentinel, raises the alarm as a broken business reference.auto-org-admin-grant.ts:785says "[sys_membergrade changes lose the true actor — thread attribution through the better-auth seam (validity windows deliberately NOT added) #4586] The provenance the row already had a column for". It also quotes ADR-0118 D1 (:786–788): "an id or null, never a sentinel".sys_user_position.granted_by(sys-user-position.object.ts:106);sys_record_share.granted_by(sys-record-share.object.ts:230).Each needs its own measurement.
Fix (the dev's draft; measure first)
readonly: truetogranted_by, and to each family member after measuring it.delegated-admin-gate.ts(:645, :781, :820) stampsgranted_by = ctx.userIdonly when the value is null ("never overwrite an explicit value").readonly, an explicit caller value that the gate leaves alone is stripped bystripReadonlyFields, so the row lands NULL.stripReadonlyFieldsusesObject.isto tell a hook write from a caller write, so a hook cannot clear a readonly field the caller also sent as null #14088's hook-write record must be shown to keep the gate's stamp.invitation-placement.ts:204,auto-org-admin-grant.ts:790andbootstrap-platform-admin.ts:1138.readonly, and the gate always stamps the writer on non-system inserts.readonlyonly, relying onstripReadonlyFieldsusesObject.isto tell a hook write from a caller write, so a hook cannot clear a readonly field the caller also sent as null #14088.readonly, with an audit-side exemption.granted_bynames a deleted user lands inprovenance. As a positive control, a business lookup still lands indangling.Not in scope: the production row holds a sentinel string. Nulling it is a production write, and it needs cloud's maintainer.
Done when
granted_bythat resolves to nothing is filed underprovenance, and the WARN no longer fires on it.Reach and timing
56bf27afuntil v18 (decision: open v18 now and ship it in stages — release the last 17.x from main first without waiting for #21908's deny (A), skip the last 17.x (B), or keep #22009's order (C)? #22050, ruling B), so the fix reaches cloud's alarm with v18.sys_import_jobis created. After that, this column is the WARN's last finding.Reader
Triage routes it. By its package it lands in the
plugin-securitylane.Dedupe
A semantic search for
granted_by readonly provenance dangling reference audit sys_user_permission_setfinds 27 issues, all of them adjacent and none this defect (the closest are #21861, #21669 and #21455). The dev's dedupe words are:granted_by readonly provenance,dangling-reference audit granted_by,db-cutover-superadminanddelegated-admin-gate granted_by explicit value.Generated by Claude Code