finding: every sys_record_share grant row lands organization_id NULL — SharingService writes under a bare system context and the row literal never carries the column #14484

Description

@os-musk

Filed unassigned by the domain:engine execution seat while running the read-side census for #13564 (measurement card, no code lands there). Recorded here rather than folded into that card, per its Zone 1.

Measured read-only on objectstack-ai/objectstack@9e286e248866c40d2db662f69aa0ba6e71b4b096.

The observation

packages/plugins/plugin-sharing/src/sharing-service.ts is the only writer of sys_record_share in this repository, and it writes under a bare system context:

  • :1261await this.engine.insert('sys_record_share', row, { context: SYSTEM_CTX });
  • :1242 — the update half, same context
  • SYSTEM_CTX at :66 is { isSystem: true, positions: [], permissions: [] } — no tenantId

The inserted row literal (:1246:1259) carries id, object_name, record_id, recipient_type, recipient_id, access_level, source, source_id, granted_by, reason, created_at, updated_at — and no organization_id. git grep organization_id over the whole file returns exactly one hit, in an unrelated doc comment at :118.

Nothing else stamps it: SqlDriver.injectTenantOnInsert only fires when DriverOptions.tenantId is present, and ObjectQLEngine.buildDriverOptions only sets that when execCtx.tenantId !== undefined. A bare { isSystem: true } context therefore reaches the driver with no tenant to stamp from.

Every sys_record_share row on every deployment is written organization_id = NULL, including grants materialised by the sharing-rule evaluator.

sys_record_share carries the tenant column (it is one of the 59 platform objects resolveTenantField resolves), and it is unclassified in the #13491 per-object tenancy ledger (packages/objectql/src/tenancy/platform-object-tenancy.ts) — so no writer-repair or design fact has ever been recorded for it either way.

Why it is worth a card rather than a shrug

The object is not currently broken by this: its readers are unscoped too (11 bare-SYSTEM_CTX read sites in the same service), so writes and reads agree today. What the NULL costs is elsewhere:

  1. Tenant attribution on a grant table. A record-share grant is the row that answers "who was given access to this record" — and it currently answers it without saying in which organization.
  2. It is a live member of the NULL-org producer class the 2026-08-31 ruling on design: isSystem 写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 addresses, on an object the ledger has not adjudicated.
  3. The project's own reading of what a NULL organization means under a wall is in the same file, [#6139] at :1500: "under single it is the one implicit tenant and DEPTH resolves normally; under group/isolated it is a missing constraint and the resolver's fail-closed obligation applies."
  4. Any future tenant-facing read of this object inherits plugin-security's Layer 0, whose strict organization_id = :tenant AND-composes over the driver's NULL-tolerant arm and wins — the exact asymmetry packages/services/service-storage/src/backfill-sys-file-organizations.ts was ordered to repair for sys_file.

What this finding does NOT claim

  • ⛔ Not a measured cross-tenant read. No database or runtime was available in this session.
  • ⛔ No repair is proposed. sys_file needed a maintainer order per table for its backfill (2026-08-28); the precedent in backfill-sys-file-organizations.ts says so in terms and forbids extending a sweep to a second table without one. Whether sys_record_share should be stamped forward, backfilled, or ruled legitimately org-less is a decision, not a measurement.

Duplicate search

Searched before filing (channel proven live — 36 results). Nearest neighbours, none of which is this: #10119 (an org-stamped rule's criteria sweep runs unscoped — the read side, and criteriaContext now addresses it), #8208 (a record created with no active organization), #11670 / #7676 (org-less rows on sys_permission_set / sys_sharing_rule), #11611 (the general "platform tables never carry organization_id — by design, or planned?" question). No open or closed issue names sys_record_share's writer.

Left ungraded and unassigned — domain:*, type and priority are triage's.

<!-- os-decision-facets -->
① 项目长远合理性(权重 ≥50%):一张授权表说不出「这条授权属于哪个组织」,是租户模型的根问题,不是记账瑕疵。而且它在 #13491 台账里是 unclassified —— 从来没人裁过它到底该不该带组织。⇒ 任何方向的裁定都把一个未判项变成已判项,是缩小不确定面。⚠️ 但「只盖章前推、不 backfill」会留下两套语义:新行有组织、存量行 NULL,读侧一收紧就分叉。
② 实际业务拉动:⚠️今天为零,而这是本卡最重要的诚实之处 —— 读侧也不带租户(同文件 11 处裸 SYSTEM_CTX 读),读写自洽,没有一位客户撞上;卡自己写明 ⛔ 不是实测的跨租户读取。
③ 防 AI 犯错:这才是真正的风险面。任何人将来给这张表加一个面向租户的读,就继承 plugin-security 的 Layer 0 严格 organization_id = :tenant,它与驱动那条容忍 NULL 的臂 AND 合成并且赢 ⇒ 所有存量授权在那一刻静默消失(不是响亮拒绝,是「这个人本来就没被授权过」)。sys_file 当初被下令 backfill,正是同一条不对称。
④ 创业阶段不扩散:「裁定它合法地无组织」零新增,但必须写进 #13491 台账,否则下一次审计再问一遍;「盖章 + backfill」是一次性数据迁移一条永久写入义务。

推荐:先裁范围,再裁方向。(a) 无论后续走哪支都该做、且不动数据的一步:把 sys_record_share#13491 台账的 unclassified 移出并写明理由;(b) 方向本身按 sys_file 先例逐表下令 —— 盖章前推 + backfill,还是裁定合法无组织。⛔ 席位不代裁:碰租户隔离边界,且一支含存量数据迁移,双重人工地板。

置信缺口(本分析看不见什么):没有数据库或运行时 ⇒「是否真能跨租户读到」既未证实也未证伪;也读不到现存部署里 sys_record_share 有多少行,而 backfill 的代价完全取决于那个数。

Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    finding: every sys_record_share grant row lands organization_id NULL — SharingService writes under a bare system context and the row literal never carries the column #14484

    Description

    @os-musk

    Filed unassigned by the domain:engine execution seat while running the read-side census for #13564 (measurement card, no code lands there). Recorded here rather than folded into that card, per its Zone 1.

    Measured read-only on objectstack-ai/objectstack@9e286e248866c40d2db662f69aa0ba6e71b4b096.

    The observation

    packages/plugins/plugin-sharing/src/sharing-service.ts is the only writer of sys_record_share in this repository, and it writes under a bare system context:

    • :1261await this.engine.insert('sys_record_share', row, { context: SYSTEM_CTX });
    • :1242 — the update half, same context
    • SYSTEM_CTX at :66 is { isSystem: true, positions: [], permissions: [] } — no tenantId

    The inserted row literal (:1246:1259) carries id, object_name, record_id, recipient_type, recipient_id, access_level, source, source_id, granted_by, reason, created_at, updated_at — and no organization_id. git grep organization_id over the whole file returns exactly one hit, in an unrelated doc comment at :118.

    Nothing else stamps it: SqlDriver.injectTenantOnInsert only fires when DriverOptions.tenantId is present, and ObjectQLEngine.buildDriverOptions only sets that when execCtx.tenantId !== undefined. A bare { isSystem: true } context therefore reaches the driver with no tenant to stamp from.

    Every sys_record_share row on every deployment is written organization_id = NULL, including grants materialised by the sharing-rule evaluator.

    sys_record_share carries the tenant column (it is one of the 59 platform objects resolveTenantField resolves), and it is unclassified in the #13491 per-object tenancy ledger (packages/objectql/src/tenancy/platform-object-tenancy.ts) — so no writer-repair or design fact has ever been recorded for it either way.

    Why it is worth a card rather than a shrug

    The object is not currently broken by this: its readers are unscoped too (11 bare-SYSTEM_CTX read sites in the same service), so writes and reads agree today. What the NULL costs is elsewhere:

    1. Tenant attribution on a grant table. A record-share grant is the row that answers "who was given access to this record" — and it currently answers it without saying in which organization.
    2. It is a live member of the NULL-org producer class the 2026-08-31 ruling on design: isSystem 写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 addresses, on an object the ledger has not adjudicated.
    3. The project's own reading of what a NULL organization means under a wall is in the same file, [#6139] at :1500: "under single it is the one implicit tenant and DEPTH resolves normally; under group/isolated it is a missing constraint and the resolver's fail-closed obligation applies."
    4. Any future tenant-facing read of this object inherits plugin-security's Layer 0, whose strict organization_id = :tenant AND-composes over the driver's NULL-tolerant arm and wins — the exact asymmetry packages/services/service-storage/src/backfill-sys-file-organizations.ts was ordered to repair for sys_file.

    What this finding does NOT claim

    • ⛔ Not a measured cross-tenant read. No database or runtime was available in this session.
    • ⛔ No repair is proposed. sys_file needed a maintainer order per table for its backfill (2026-08-28); the precedent in backfill-sys-file-organizations.ts says so in terms and forbids extending a sweep to a second table without one. Whether sys_record_share should be stamped forward, backfilled, or ruled legitimately org-less is a decision, not a measurement.

    Duplicate search

    Searched before filing (channel proven live — 36 results). Nearest neighbours, none of which is this: #10119 (an org-stamped rule's criteria sweep runs unscoped — the read side, and criteriaContext now addresses it), #8208 (a record created with no active organization), #11670 / #7676 (org-less rows on sys_permission_set / sys_sharing_rule), #11611 (the general "platform tables never carry organization_id — by design, or planned?" question). No open or closed issue names sys_record_share's writer.

    Left ungraded and unassigned — domain:*, type and priority are triage's.

    <!-- os-decision-facets -->
    ① 项目长远合理性(权重 ≥50%):一张授权表说不出「这条授权属于哪个组织」,是租户模型的根问题,不是记账瑕疵。而且它在 #13491 台账里是 unclassified —— 从来没人裁过它到底该不该带组织。⇒ 任何方向的裁定都把一个未判项变成已判项,是缩小不确定面。⚠️ 但「只盖章前推、不 backfill」会留下两套语义:新行有组织、存量行 NULL,读侧一收紧就分叉。
    ② 实际业务拉动:⚠️今天为零,而这是本卡最重要的诚实之处 —— 读侧也不带租户(同文件 11 处裸 SYSTEM_CTX 读),读写自洽,没有一位客户撞上;卡自己写明 ⛔ 不是实测的跨租户读取。
    ③ 防 AI 犯错:这才是真正的风险面。任何人将来给这张表加一个面向租户的读,就继承 plugin-security 的 Layer 0 严格 organization_id = :tenant,它与驱动那条容忍 NULL 的臂 AND 合成并且赢 ⇒ 所有存量授权在那一刻静默消失(不是响亮拒绝,是「这个人本来就没被授权过」)。sys_file 当初被下令 backfill,正是同一条不对称。
    ④ 创业阶段不扩散:「裁定它合法地无组织」零新增,但必须写进 #13491 台账,否则下一次审计再问一遍;「盖章 + backfill」是一次性数据迁移一条永久写入义务。

    推荐:先裁范围,再裁方向。(a) 无论后续走哪支都该做、且不动数据的一步:把 sys_record_share#13491 台账的 unclassified 移出并写明理由;(b) 方向本身按 sys_file 先例逐表下令 —— 盖章前推 + backfill,还是裁定合法无组织。⛔ 席位不代裁:碰租户隔离边界,且一支含存量数据迁移,双重人工地板。

    置信缺口(本分析看不见什么):没有数据库或运行时 ⇒「是否真能跨租户读到」既未证实也未证伪;也读不到现存部署里 sys_record_share 有多少行,而 backfill 的代价完全取决于那个数。

    Generated by Claude Code

    Metadata

    Metadata

    Assignees

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      finding: every sys_record_share grant row lands organization_id NULL — SharingService writes under a bare system context and the row literal never carries the column #14484

      Description

      @os-musk

      Filed unassigned by the domain:engine execution seat while running the read-side census for #13564 (measurement card, no code lands there). Recorded here rather than folded into that card, per its Zone 1.

      Measured read-only on objectstack-ai/objectstack@9e286e248866c40d2db662f69aa0ba6e71b4b096.

      The observation

      packages/plugins/plugin-sharing/src/sharing-service.ts is the only writer of sys_record_share in this repository, and it writes under a bare system context:

      • :1261await this.engine.insert('sys_record_share', row, { context: SYSTEM_CTX });
      • :1242 — the update half, same context
      • SYSTEM_CTX at :66 is { isSystem: true, positions: [], permissions: [] } — no tenantId

      The inserted row literal (:1246:1259) carries id, object_name, record_id, recipient_type, recipient_id, access_level, source, source_id, granted_by, reason, created_at, updated_at — and no organization_id. git grep organization_id over the whole file returns exactly one hit, in an unrelated doc comment at :118.

      Nothing else stamps it: SqlDriver.injectTenantOnInsert only fires when DriverOptions.tenantId is present, and ObjectQLEngine.buildDriverOptions only sets that when execCtx.tenantId !== undefined. A bare { isSystem: true } context therefore reaches the driver with no tenant to stamp from.

      Every sys_record_share row on every deployment is written organization_id = NULL, including grants materialised by the sharing-rule evaluator.

      sys_record_share carries the tenant column (it is one of the 59 platform objects resolveTenantField resolves), and it is unclassified in the #13491 per-object tenancy ledger (packages/objectql/src/tenancy/platform-object-tenancy.ts) — so no writer-repair or design fact has ever been recorded for it either way.

      Why it is worth a card rather than a shrug

      The object is not currently broken by this: its readers are unscoped too (11 bare-SYSTEM_CTX read sites in the same service), so writes and reads agree today. What the NULL costs is elsewhere:

      1. Tenant attribution on a grant table. A record-share grant is the row that answers "who was given access to this record" — and it currently answers it without saying in which organization.
      2. It is a live member of the NULL-org producer class the 2026-08-31 ruling on design: isSystem 写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 addresses, on an object the ledger has not adjudicated.
      3. The project's own reading of what a NULL organization means under a wall is in the same file, [#6139] at :1500: "under single it is the one implicit tenant and DEPTH resolves normally; under group/isolated it is a missing constraint and the resolver's fail-closed obligation applies."
      4. Any future tenant-facing read of this object inherits plugin-security's Layer 0, whose strict organization_id = :tenant AND-composes over the driver's NULL-tolerant arm and wins — the exact asymmetry packages/services/service-storage/src/backfill-sys-file-organizations.ts was ordered to repair for sys_file.

      What this finding does NOT claim

      • ⛔ Not a measured cross-tenant read. No database or runtime was available in this session.
      • ⛔ No repair is proposed. sys_file needed a maintainer order per table for its backfill (2026-08-28); the precedent in backfill-sys-file-organizations.ts says so in terms and forbids extending a sweep to a second table without one. Whether sys_record_share should be stamped forward, backfilled, or ruled legitimately org-less is a decision, not a measurement.

      Duplicate search

      Searched before filing (channel proven live — 36 results). Nearest neighbours, none of which is this: #10119 (an org-stamped rule's criteria sweep runs unscoped — the read side, and criteriaContext now addresses it), #8208 (a record created with no active organization), #11670 / #7676 (org-less rows on sys_permission_set / sys_sharing_rule), #11611 (the general "platform tables never carry organization_id — by design, or planned?" question). No open or closed issue names sys_record_share's writer.

      Left ungraded and unassigned — domain:*, type and priority are triage's.

      <!-- os-decision-facets -->
      ① 项目长远合理性(权重 ≥50%):一张授权表说不出「这条授权属于哪个组织」,是租户模型的根问题,不是记账瑕疵。而且它在 #13491 台账里是 unclassified —— 从来没人裁过它到底该不该带组织。⇒ 任何方向的裁定都把一个未判项变成已判项,是缩小不确定面。⚠️ 但「只盖章前推、不 backfill」会留下两套语义:新行有组织、存量行 NULL,读侧一收紧就分叉。
      ② 实际业务拉动:⚠️今天为零,而这是本卡最重要的诚实之处 —— 读侧也不带租户(同文件 11 处裸 SYSTEM_CTX 读),读写自洽,没有一位客户撞上;卡自己写明 ⛔ 不是实测的跨租户读取。
      ③ 防 AI 犯错:这才是真正的风险面。任何人将来给这张表加一个面向租户的读,就继承 plugin-security 的 Layer 0 严格 organization_id = :tenant,它与驱动那条容忍 NULL 的臂 AND 合成并且赢 ⇒ 所有存量授权在那一刻静默消失(不是响亮拒绝,是「这个人本来就没被授权过」)。sys_file 当初被下令 backfill,正是同一条不对称。
      ④ 创业阶段不扩散:「裁定它合法地无组织」零新增,但必须写进 #13491 台账,否则下一次审计再问一遍;「盖章 + backfill」是一次性数据迁移一条永久写入义务。

      推荐:先裁范围,再裁方向。(a) 无论后续走哪支都该做、且不动数据的一步:把 sys_record_share#13491 台账的 unclassified 移出并写明理由;(b) 方向本身按 sys_file 先例逐表下令 —— 盖章前推 + backfill,还是裁定合法无组织。⛔ 席位不代裁:碰租户隔离边界,且一支含存量数据迁移,双重人工地板。

      置信缺口(本分析看不见什么):没有数据库或运行时 ⇒「是否真能跨租户读到」既未证实也未证伪;也读不到现存部署里 sys_record_share 有多少行,而 backfill 的代价完全取决于那个数。

      Generated by Claude Code

      Metadata

      Metadata

      Assignees

      Type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        finding: every sys_record_share grant row lands organization_id NULL — SharingService writes under a bare system context and the row literal never carries the column #14484

        Description

        @os-musk

        Filed unassigned by the domain:engine execution seat while running the read-side census for #13564 (measurement card, no code lands there). Recorded here rather than folded into that card, per its Zone 1.

        Measured read-only on objectstack-ai/objectstack@9e286e248866c40d2db662f69aa0ba6e71b4b096.

        The observation

        packages/plugins/plugin-sharing/src/sharing-service.ts is the only writer of sys_record_share in this repository, and it writes under a bare system context:

        • :1261await this.engine.insert('sys_record_share', row, { context: SYSTEM_CTX });
        • :1242 — the update half, same context
        • SYSTEM_CTX at :66 is { isSystem: true, positions: [], permissions: [] } — no tenantId

        The inserted row literal (:1246:1259) carries id, object_name, record_id, recipient_type, recipient_id, access_level, source, source_id, granted_by, reason, created_at, updated_at — and no organization_id. git grep organization_id over the whole file returns exactly one hit, in an unrelated doc comment at :118.

        Nothing else stamps it: SqlDriver.injectTenantOnInsert only fires when DriverOptions.tenantId is present, and ObjectQLEngine.buildDriverOptions only sets that when execCtx.tenantId !== undefined. A bare { isSystem: true } context therefore reaches the driver with no tenant to stamp from.

        Every sys_record_share row on every deployment is written organization_id = NULL, including grants materialised by the sharing-rule evaluator.

        sys_record_share carries the tenant column (it is one of the 59 platform objects resolveTenantField resolves), and it is unclassified in the #13491 per-object tenancy ledger (packages/objectql/src/tenancy/platform-object-tenancy.ts) — so no writer-repair or design fact has ever been recorded for it either way.

        Why it is worth a card rather than a shrug

        The object is not currently broken by this: its readers are unscoped too (11 bare-SYSTEM_CTX read sites in the same service), so writes and reads agree today. What the NULL costs is elsewhere:

        1. Tenant attribution on a grant table. A record-share grant is the row that answers "who was given access to this record" — and it currently answers it without saying in which organization.
        2. It is a live member of the NULL-org producer class the 2026-08-31 ruling on design: isSystem 写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 addresses, on an object the ledger has not adjudicated.
        3. The project's own reading of what a NULL organization means under a wall is in the same file, [#6139] at :1500: "under single it is the one implicit tenant and DEPTH resolves normally; under group/isolated it is a missing constraint and the resolver's fail-closed obligation applies."
        4. Any future tenant-facing read of this object inherits plugin-security's Layer 0, whose strict organization_id = :tenant AND-composes over the driver's NULL-tolerant arm and wins — the exact asymmetry packages/services/service-storage/src/backfill-sys-file-organizations.ts was ordered to repair for sys_file.

        What this finding does NOT claim

        • ⛔ Not a measured cross-tenant read. No database or runtime was available in this session.
        • ⛔ No repair is proposed. sys_file needed a maintainer order per table for its backfill (2026-08-28); the precedent in backfill-sys-file-organizations.ts says so in terms and forbids extending a sweep to a second table without one. Whether sys_record_share should be stamped forward, backfilled, or ruled legitimately org-less is a decision, not a measurement.

        Duplicate search

        Searched before filing (channel proven live — 36 results). Nearest neighbours, none of which is this: #10119 (an org-stamped rule's criteria sweep runs unscoped — the read side, and criteriaContext now addresses it), #8208 (a record created with no active organization), #11670 / #7676 (org-less rows on sys_permission_set / sys_sharing_rule), #11611 (the general "platform tables never carry organization_id — by design, or planned?" question). No open or closed issue names sys_record_share's writer.

        Left ungraded and unassigned — domain:*, type and priority are triage's.

        <!-- os-decision-facets -->
        ① 项目长远合理性(权重 ≥50%):一张授权表说不出「这条授权属于哪个组织」,是租户模型的根问题,不是记账瑕疵。而且它在 #13491 台账里是 unclassified —— 从来没人裁过它到底该不该带组织。⇒ 任何方向的裁定都把一个未判项变成已判项,是缩小不确定面。⚠️ 但「只盖章前推、不 backfill」会留下两套语义:新行有组织、存量行 NULL,读侧一收紧就分叉。
        ② 实际业务拉动:⚠️今天为零,而这是本卡最重要的诚实之处 —— 读侧也不带租户(同文件 11 处裸 SYSTEM_CTX 读),读写自洽,没有一位客户撞上;卡自己写明 ⛔ 不是实测的跨租户读取。
        ③ 防 AI 犯错:这才是真正的风险面。任何人将来给这张表加一个面向租户的读,就继承 plugin-security 的 Layer 0 严格 organization_id = :tenant,它与驱动那条容忍 NULL 的臂 AND 合成并且赢 ⇒ 所有存量授权在那一刻静默消失(不是响亮拒绝,是「这个人本来就没被授权过」)。sys_file 当初被下令 backfill,正是同一条不对称。
        ④ 创业阶段不扩散:「裁定它合法地无组织」零新增,但必须写进 #13491 台账,否则下一次审计再问一遍;「盖章 + backfill」是一次性数据迁移一条永久写入义务。

        推荐:先裁范围,再裁方向。(a) 无论后续走哪支都该做、且不动数据的一步:把 sys_record_share#13491 台账的 unclassified 移出并写明理由;(b) 方向本身按 sys_file 先例逐表下令 —— 盖章前推 + backfill,还是裁定合法无组织。⛔ 席位不代裁:碰租户隔离边界,且一支含存量数据迁移,双重人工地板。

        置信缺口(本分析看不见什么):没有数据库或运行时 ⇒「是否真能跨租户读到」既未证实也未证伪;也读不到现存部署里 sys_record_share 有多少行,而 backfill 的代价完全取决于那个数。

        Generated by Claude Code

        Metadata

        Metadata

        Assignees

        Type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          finding: every sys_record_share grant row lands organization_id NULL — SharingService writes under a bare system context and the row literal never carries the column #14484

          Description

          @os-musk

          Filed unassigned by the domain:engine execution seat while running the read-side census for #13564 (measurement card, no code lands there). Recorded here rather than folded into that card, per its Zone 1.

          Measured read-only on objectstack-ai/objectstack@9e286e248866c40d2db662f69aa0ba6e71b4b096.

          The observation

          packages/plugins/plugin-sharing/src/sharing-service.ts is the only writer of sys_record_share in this repository, and it writes under a bare system context:

          • :1261await this.engine.insert('sys_record_share', row, { context: SYSTEM_CTX });
          • :1242 — the update half, same context
          • SYSTEM_CTX at :66 is { isSystem: true, positions: [], permissions: [] } — no tenantId

          The inserted row literal (:1246:1259) carries id, object_name, record_id, recipient_type, recipient_id, access_level, source, source_id, granted_by, reason, created_at, updated_at — and no organization_id. git grep organization_id over the whole file returns exactly one hit, in an unrelated doc comment at :118.

          Nothing else stamps it: SqlDriver.injectTenantOnInsert only fires when DriverOptions.tenantId is present, and ObjectQLEngine.buildDriverOptions only sets that when execCtx.tenantId !== undefined. A bare { isSystem: true } context therefore reaches the driver with no tenant to stamp from.

          Every sys_record_share row on every deployment is written organization_id = NULL, including grants materialised by the sharing-rule evaluator.

          sys_record_share carries the tenant column (it is one of the 59 platform objects resolveTenantField resolves), and it is unclassified in the #13491 per-object tenancy ledger (packages/objectql/src/tenancy/platform-object-tenancy.ts) — so no writer-repair or design fact has ever been recorded for it either way.

          Why it is worth a card rather than a shrug

          The object is not currently broken by this: its readers are unscoped too (11 bare-SYSTEM_CTX read sites in the same service), so writes and reads agree today. What the NULL costs is elsewhere:

          1. Tenant attribution on a grant table. A record-share grant is the row that answers "who was given access to this record" — and it currently answers it without saying in which organization.
          2. It is a live member of the NULL-org producer class the 2026-08-31 ruling on design: isSystem 写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 addresses, on an object the ledger has not adjudicated.
          3. The project's own reading of what a NULL organization means under a wall is in the same file, [#6139] at :1500: "under single it is the one implicit tenant and DEPTH resolves normally; under group/isolated it is a missing constraint and the resolver's fail-closed obligation applies."
          4. Any future tenant-facing read of this object inherits plugin-security's Layer 0, whose strict organization_id = :tenant AND-composes over the driver's NULL-tolerant arm and wins — the exact asymmetry packages/services/service-storage/src/backfill-sys-file-organizations.ts was ordered to repair for sys_file.

          What this finding does NOT claim

          • ⛔ Not a measured cross-tenant read. No database or runtime was available in this session.
          • ⛔ No repair is proposed. sys_file needed a maintainer order per table for its backfill (2026-08-28); the precedent in backfill-sys-file-organizations.ts says so in terms and forbids extending a sweep to a second table without one. Whether sys_record_share should be stamped forward, backfilled, or ruled legitimately org-less is a decision, not a measurement.

          Duplicate search

          Searched before filing (channel proven live — 36 results). Nearest neighbours, none of which is this: #10119 (an org-stamped rule's criteria sweep runs unscoped — the read side, and criteriaContext now addresses it), #8208 (a record created with no active organization), #11670 / #7676 (org-less rows on sys_permission_set / sys_sharing_rule), #11611 (the general "platform tables never carry organization_id — by design, or planned?" question). No open or closed issue names sys_record_share's writer.

          Left ungraded and unassigned — domain:*, type and priority are triage's.

          <!-- os-decision-facets -->
          ① 项目长远合理性(权重 ≥50%):一张授权表说不出「这条授权属于哪个组织」,是租户模型的根问题,不是记账瑕疵。而且它在 #13491 台账里是 unclassified —— 从来没人裁过它到底该不该带组织。⇒ 任何方向的裁定都把一个未判项变成已判项,是缩小不确定面。⚠️ 但「只盖章前推、不 backfill」会留下两套语义:新行有组织、存量行 NULL,读侧一收紧就分叉。
          ② 实际业务拉动:⚠️今天为零,而这是本卡最重要的诚实之处 —— 读侧也不带租户(同文件 11 处裸 SYSTEM_CTX 读),读写自洽,没有一位客户撞上;卡自己写明 ⛔ 不是实测的跨租户读取。
          ③ 防 AI 犯错:这才是真正的风险面。任何人将来给这张表加一个面向租户的读,就继承 plugin-security 的 Layer 0 严格 organization_id = :tenant,它与驱动那条容忍 NULL 的臂 AND 合成并且赢 ⇒ 所有存量授权在那一刻静默消失(不是响亮拒绝,是「这个人本来就没被授权过」)。sys_file 当初被下令 backfill,正是同一条不对称。
          ④ 创业阶段不扩散:「裁定它合法地无组织」零新增,但必须写进 #13491 台账,否则下一次审计再问一遍;「盖章 + backfill」是一次性数据迁移一条永久写入义务。

          推荐:先裁范围,再裁方向。(a) 无论后续走哪支都该做、且不动数据的一步:把 sys_record_share#13491 台账的 unclassified 移出并写明理由;(b) 方向本身按 sys_file 先例逐表下令 —— 盖章前推 + backfill,还是裁定合法无组织。⛔ 席位不代裁:碰租户隔离边界,且一支含存量数据迁移,双重人工地板。

          置信缺口(本分析看不见什么):没有数据库或运行时 ⇒「是否真能跨租户读到」既未证实也未证伪;也读不到现存部署里 sys_record_share 有多少行,而 backfill 的代价完全取决于那个数。

          Generated by Claude Code

          Metadata

          Metadata

          Assignees

          Type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            finding: every sys_record_share grant row lands organization_id NULL — SharingService writes under a bare system context and the row literal never carries the column #14484

            Description

            @os-musk

            Filed unassigned by the domain:engine execution seat while running the read-side census for #13564 (measurement card, no code lands there). Recorded here rather than folded into that card, per its Zone 1.

            Measured read-only on objectstack-ai/objectstack@9e286e248866c40d2db662f69aa0ba6e71b4b096.

            The observation

            packages/plugins/plugin-sharing/src/sharing-service.ts is the only writer of sys_record_share in this repository, and it writes under a bare system context:

            • :1261await this.engine.insert('sys_record_share', row, { context: SYSTEM_CTX });
            • :1242 — the update half, same context
            • SYSTEM_CTX at :66 is { isSystem: true, positions: [], permissions: [] } — no tenantId

            The inserted row literal (:1246:1259) carries id, object_name, record_id, recipient_type, recipient_id, access_level, source, source_id, granted_by, reason, created_at, updated_at — and no organization_id. git grep organization_id over the whole file returns exactly one hit, in an unrelated doc comment at :118.

            Nothing else stamps it: SqlDriver.injectTenantOnInsert only fires when DriverOptions.tenantId is present, and ObjectQLEngine.buildDriverOptions only sets that when execCtx.tenantId !== undefined. A bare { isSystem: true } context therefore reaches the driver with no tenant to stamp from.

            Every sys_record_share row on every deployment is written organization_id = NULL, including grants materialised by the sharing-rule evaluator.

            sys_record_share carries the tenant column (it is one of the 59 platform objects resolveTenantField resolves), and it is unclassified in the #13491 per-object tenancy ledger (packages/objectql/src/tenancy/platform-object-tenancy.ts) — so no writer-repair or design fact has ever been recorded for it either way.

            Why it is worth a card rather than a shrug

            The object is not currently broken by this: its readers are unscoped too (11 bare-SYSTEM_CTX read sites in the same service), so writes and reads agree today. What the NULL costs is elsewhere:

            1. Tenant attribution on a grant table. A record-share grant is the row that answers "who was given access to this record" — and it currently answers it without saying in which organization.
            2. It is a live member of the NULL-org producer class the 2026-08-31 ruling on design: isSystem 写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 addresses, on an object the ledger has not adjudicated.
            3. The project's own reading of what a NULL organization means under a wall is in the same file, [#6139] at :1500: "under single it is the one implicit tenant and DEPTH resolves normally; under group/isolated it is a missing constraint and the resolver's fail-closed obligation applies."
            4. Any future tenant-facing read of this object inherits plugin-security's Layer 0, whose strict organization_id = :tenant AND-composes over the driver's NULL-tolerant arm and wins — the exact asymmetry packages/services/service-storage/src/backfill-sys-file-organizations.ts was ordered to repair for sys_file.

            What this finding does NOT claim

            • ⛔ Not a measured cross-tenant read. No database or runtime was available in this session.
            • ⛔ No repair is proposed. sys_file needed a maintainer order per table for its backfill (2026-08-28); the precedent in backfill-sys-file-organizations.ts says so in terms and forbids extending a sweep to a second table without one. Whether sys_record_share should be stamped forward, backfilled, or ruled legitimately org-less is a decision, not a measurement.

            Duplicate search

            Searched before filing (channel proven live — 36 results). Nearest neighbours, none of which is this: #10119 (an org-stamped rule's criteria sweep runs unscoped — the read side, and criteriaContext now addresses it), #8208 (a record created with no active organization), #11670 / #7676 (org-less rows on sys_permission_set / sys_sharing_rule), #11611 (the general "platform tables never carry organization_id — by design, or planned?" question). No open or closed issue names sys_record_share's writer.

            Left ungraded and unassigned — domain:*, type and priority are triage's.

            <!-- os-decision-facets -->
            ① 项目长远合理性(权重 ≥50%):一张授权表说不出「这条授权属于哪个组织」,是租户模型的根问题,不是记账瑕疵。而且它在 #13491 台账里是 unclassified —— 从来没人裁过它到底该不该带组织。⇒ 任何方向的裁定都把一个未判项变成已判项,是缩小不确定面。⚠️ 但「只盖章前推、不 backfill」会留下两套语义:新行有组织、存量行 NULL,读侧一收紧就分叉。
            ② 实际业务拉动:⚠️今天为零,而这是本卡最重要的诚实之处 —— 读侧也不带租户(同文件 11 处裸 SYSTEM_CTX 读),读写自洽,没有一位客户撞上;卡自己写明 ⛔ 不是实测的跨租户读取。
            ③ 防 AI 犯错:这才是真正的风险面。任何人将来给这张表加一个面向租户的读,就继承 plugin-security 的 Layer 0 严格 organization_id = :tenant,它与驱动那条容忍 NULL 的臂 AND 合成并且赢 ⇒ 所有存量授权在那一刻静默消失(不是响亮拒绝,是「这个人本来就没被授权过」)。sys_file 当初被下令 backfill,正是同一条不对称。
            ④ 创业阶段不扩散:「裁定它合法地无组织」零新增,但必须写进 #13491 台账,否则下一次审计再问一遍;「盖章 + backfill」是一次性数据迁移一条永久写入义务。

            推荐:先裁范围,再裁方向。(a) 无论后续走哪支都该做、且不动数据的一步:把 sys_record_share#13491 台账的 unclassified 移出并写明理由;(b) 方向本身按 sys_file 先例逐表下令 —— 盖章前推 + backfill,还是裁定合法无组织。⛔ 席位不代裁:碰租户隔离边界,且一支含存量数据迁移,双重人工地板。

            置信缺口(本分析看不见什么):没有数据库或运行时 ⇒「是否真能跨租户读到」既未证实也未证伪;也读不到现存部署里 sys_record_share 有多少行,而 backfill 的代价完全取决于那个数。

            Generated by Claude Code

            Metadata

            Metadata

            Assignees

            Type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              finding: every sys_record_share grant row lands organization_id NULL — SharingService writes under a bare system context and the row literal never carries the column #14484

              Description

              @os-musk

              Filed unassigned by the domain:engine execution seat while running the read-side census for #13564 (measurement card, no code lands there). Recorded here rather than folded into that card, per its Zone 1.

              Measured read-only on objectstack-ai/objectstack@9e286e248866c40d2db662f69aa0ba6e71b4b096.

              The observation

              packages/plugins/plugin-sharing/src/sharing-service.ts is the only writer of sys_record_share in this repository, and it writes under a bare system context:

              • :1261await this.engine.insert('sys_record_share', row, { context: SYSTEM_CTX });
              • :1242 — the update half, same context
              • SYSTEM_CTX at :66 is { isSystem: true, positions: [], permissions: [] } — no tenantId

              The inserted row literal (:1246:1259) carries id, object_name, record_id, recipient_type, recipient_id, access_level, source, source_id, granted_by, reason, created_at, updated_at — and no organization_id. git grep organization_id over the whole file returns exactly one hit, in an unrelated doc comment at :118.

              Nothing else stamps it: SqlDriver.injectTenantOnInsert only fires when DriverOptions.tenantId is present, and ObjectQLEngine.buildDriverOptions only sets that when execCtx.tenantId !== undefined. A bare { isSystem: true } context therefore reaches the driver with no tenant to stamp from.

              Every sys_record_share row on every deployment is written organization_id = NULL, including grants materialised by the sharing-rule evaluator.

              sys_record_share carries the tenant column (it is one of the 59 platform objects resolveTenantField resolves), and it is unclassified in the #13491 per-object tenancy ledger (packages/objectql/src/tenancy/platform-object-tenancy.ts) — so no writer-repair or design fact has ever been recorded for it either way.

              Why it is worth a card rather than a shrug

              The object is not currently broken by this: its readers are unscoped too (11 bare-SYSTEM_CTX read sites in the same service), so writes and reads agree today. What the NULL costs is elsewhere:

              1. Tenant attribution on a grant table. A record-share grant is the row that answers "who was given access to this record" — and it currently answers it without saying in which organization.
              2. It is a live member of the NULL-org producer class the 2026-08-31 ruling on design: isSystem 写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 addresses, on an object the ledger has not adjudicated.
              3. The project's own reading of what a NULL organization means under a wall is in the same file, [#6139] at :1500: "under single it is the one implicit tenant and DEPTH resolves normally; under group/isolated it is a missing constraint and the resolver's fail-closed obligation applies."
              4. Any future tenant-facing read of this object inherits plugin-security's Layer 0, whose strict organization_id = :tenant AND-composes over the driver's NULL-tolerant arm and wins — the exact asymmetry packages/services/service-storage/src/backfill-sys-file-organizations.ts was ordered to repair for sys_file.

              What this finding does NOT claim

              • ⛔ Not a measured cross-tenant read. No database or runtime was available in this session.
              • ⛔ No repair is proposed. sys_file needed a maintainer order per table for its backfill (2026-08-28); the precedent in backfill-sys-file-organizations.ts says so in terms and forbids extending a sweep to a second table without one. Whether sys_record_share should be stamped forward, backfilled, or ruled legitimately org-less is a decision, not a measurement.

              Duplicate search

              Searched before filing (channel proven live — 36 results). Nearest neighbours, none of which is this: #10119 (an org-stamped rule's criteria sweep runs unscoped — the read side, and criteriaContext now addresses it), #8208 (a record created with no active organization), #11670 / #7676 (org-less rows on sys_permission_set / sys_sharing_rule), #11611 (the general "platform tables never carry organization_id — by design, or planned?" question). No open or closed issue names sys_record_share's writer.

              Left ungraded and unassigned — domain:*, type and priority are triage's.

              <!-- os-decision-facets -->
              ① 项目长远合理性(权重 ≥50%):一张授权表说不出「这条授权属于哪个组织」,是租户模型的根问题,不是记账瑕疵。而且它在 #13491 台账里是 unclassified —— 从来没人裁过它到底该不该带组织。⇒ 任何方向的裁定都把一个未判项变成已判项,是缩小不确定面。⚠️ 但「只盖章前推、不 backfill」会留下两套语义:新行有组织、存量行 NULL,读侧一收紧就分叉。
              ② 实际业务拉动:⚠️今天为零,而这是本卡最重要的诚实之处 —— 读侧也不带租户(同文件 11 处裸 SYSTEM_CTX 读),读写自洽,没有一位客户撞上;卡自己写明 ⛔ 不是实测的跨租户读取。
              ③ 防 AI 犯错:这才是真正的风险面。任何人将来给这张表加一个面向租户的读,就继承 plugin-security 的 Layer 0 严格 organization_id = :tenant,它与驱动那条容忍 NULL 的臂 AND 合成并且赢 ⇒ 所有存量授权在那一刻静默消失(不是响亮拒绝,是「这个人本来就没被授权过」)。sys_file 当初被下令 backfill,正是同一条不对称。
              ④ 创业阶段不扩散:「裁定它合法地无组织」零新增,但必须写进 #13491 台账,否则下一次审计再问一遍;「盖章 + backfill」是一次性数据迁移一条永久写入义务。

              推荐:先裁范围,再裁方向。(a) 无论后续走哪支都该做、且不动数据的一步:把 sys_record_share#13491 台账的 unclassified 移出并写明理由;(b) 方向本身按 sys_file 先例逐表下令 —— 盖章前推 + backfill,还是裁定合法无组织。⛔ 席位不代裁:碰租户隔离边界,且一支含存量数据迁移,双重人工地板。

              置信缺口(本分析看不见什么):没有数据库或运行时 ⇒「是否真能跨租户读到」既未证实也未证伪;也读不到现存部署里 sys_record_share 有多少行,而 backfill 的代价完全取决于那个数。

              Generated by Claude Code

              Metadata

              Metadata

              Assignees

              Type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                finding: every sys_record_share grant row lands organization_id NULL — SharingService writes under a bare system context and the row literal never carries the column #14484

                Description

                @os-musk

                Filed unassigned by the domain:engine execution seat while running the read-side census for #13564 (measurement card, no code lands there). Recorded here rather than folded into that card, per its Zone 1.

                Measured read-only on objectstack-ai/objectstack@9e286e248866c40d2db662f69aa0ba6e71b4b096.

                The observation

                packages/plugins/plugin-sharing/src/sharing-service.ts is the only writer of sys_record_share in this repository, and it writes under a bare system context:

                • :1261await this.engine.insert('sys_record_share', row, { context: SYSTEM_CTX });
                • :1242 — the update half, same context
                • SYSTEM_CTX at :66 is { isSystem: true, positions: [], permissions: [] } — no tenantId

                The inserted row literal (:1246:1259) carries id, object_name, record_id, recipient_type, recipient_id, access_level, source, source_id, granted_by, reason, created_at, updated_at — and no organization_id. git grep organization_id over the whole file returns exactly one hit, in an unrelated doc comment at :118.

                Nothing else stamps it: SqlDriver.injectTenantOnInsert only fires when DriverOptions.tenantId is present, and ObjectQLEngine.buildDriverOptions only sets that when execCtx.tenantId !== undefined. A bare { isSystem: true } context therefore reaches the driver with no tenant to stamp from.

                Every sys_record_share row on every deployment is written organization_id = NULL, including grants materialised by the sharing-rule evaluator.

                sys_record_share carries the tenant column (it is one of the 59 platform objects resolveTenantField resolves), and it is unclassified in the #13491 per-object tenancy ledger (packages/objectql/src/tenancy/platform-object-tenancy.ts) — so no writer-repair or design fact has ever been recorded for it either way.

                Why it is worth a card rather than a shrug

                The object is not currently broken by this: its readers are unscoped too (11 bare-SYSTEM_CTX read sites in the same service), so writes and reads agree today. What the NULL costs is elsewhere:

                1. Tenant attribution on a grant table. A record-share grant is the row that answers "who was given access to this record" — and it currently answers it without saying in which organization.
                2. It is a live member of the NULL-org producer class the 2026-08-31 ruling on design: isSystem 写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 addresses, on an object the ledger has not adjudicated.
                3. The project's own reading of what a NULL organization means under a wall is in the same file, [#6139] at :1500: "under single it is the one implicit tenant and DEPTH resolves normally; under group/isolated it is a missing constraint and the resolver's fail-closed obligation applies."
                4. Any future tenant-facing read of this object inherits plugin-security's Layer 0, whose strict organization_id = :tenant AND-composes over the driver's NULL-tolerant arm and wins — the exact asymmetry packages/services/service-storage/src/backfill-sys-file-organizations.ts was ordered to repair for sys_file.

                What this finding does NOT claim

                • ⛔ Not a measured cross-tenant read. No database or runtime was available in this session.
                • ⛔ No repair is proposed. sys_file needed a maintainer order per table for its backfill (2026-08-28); the precedent in backfill-sys-file-organizations.ts says so in terms and forbids extending a sweep to a second table without one. Whether sys_record_share should be stamped forward, backfilled, or ruled legitimately org-less is a decision, not a measurement.

                Duplicate search

                Searched before filing (channel proven live — 36 results). Nearest neighbours, none of which is this: #10119 (an org-stamped rule's criteria sweep runs unscoped — the read side, and criteriaContext now addresses it), #8208 (a record created with no active organization), #11670 / #7676 (org-less rows on sys_permission_set / sys_sharing_rule), #11611 (the general "platform tables never carry organization_id — by design, or planned?" question). No open or closed issue names sys_record_share's writer.

                Left ungraded and unassigned — domain:*, type and priority are triage's.

                <!-- os-decision-facets -->
                ① 项目长远合理性(权重 ≥50%):一张授权表说不出「这条授权属于哪个组织」,是租户模型的根问题,不是记账瑕疵。而且它在 #13491 台账里是 unclassified —— 从来没人裁过它到底该不该带组织。⇒ 任何方向的裁定都把一个未判项变成已判项,是缩小不确定面。⚠️ 但「只盖章前推、不 backfill」会留下两套语义:新行有组织、存量行 NULL,读侧一收紧就分叉。
                ② 实际业务拉动:⚠️今天为零,而这是本卡最重要的诚实之处 —— 读侧也不带租户(同文件 11 处裸 SYSTEM_CTX 读),读写自洽,没有一位客户撞上;卡自己写明 ⛔ 不是实测的跨租户读取。
                ③ 防 AI 犯错:这才是真正的风险面。任何人将来给这张表加一个面向租户的读,就继承 plugin-security 的 Layer 0 严格 organization_id = :tenant,它与驱动那条容忍 NULL 的臂 AND 合成并且赢 ⇒ 所有存量授权在那一刻静默消失(不是响亮拒绝,是「这个人本来就没被授权过」)。sys_file 当初被下令 backfill,正是同一条不对称。
                ④ 创业阶段不扩散:「裁定它合法地无组织」零新增,但必须写进 #13491 台账,否则下一次审计再问一遍;「盖章 + backfill」是一次性数据迁移一条永久写入义务。

                推荐:先裁范围,再裁方向。(a) 无论后续走哪支都该做、且不动数据的一步:把 sys_record_share#13491 台账的 unclassified 移出并写明理由;(b) 方向本身按 sys_file 先例逐表下令 —— 盖章前推 + backfill,还是裁定合法无组织。⛔ 席位不代裁:碰租户隔离边界,且一支含存量数据迁移,双重人工地板。

                置信缺口(本分析看不见什么):没有数据库或运行时 ⇒「是否真能跨租户读到」既未证实也未证伪;也读不到现存部署里 sys_record_share 有多少行,而 backfill 的代价完全取决于那个数。

                Generated by Claude Code

                Metadata

                Metadata

                Assignees

                Type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions