Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

plugin-grid: ObjectGrid builds the server $select from the authored projection with no FLS gate #6898

Closed

Description

@claude

ObjectGrid's data loader builds the server $select from the authored columns / fields
projection with no field-level-security gate, so a field the principal may not read can still be
REQUESTED and returned into the browser.

Filing, not claiming. Observed while implementing objectui#6799 (the authored-columns FLS
re-check), which the maintainer ruling of 2026-08-30 deliberately scoped to generateColumns()
and to that function only. This is the same defect family one layer down, and it is explicitly
out of scope there.

Where

packages/plugin-grid/src/ObjectGrid.tsx, loadSchemaAndData() — getSelectFields() (around
L1402 and the getSelectFields closure below it). It resolves each column's identity through
columnIdentity, adds id, adds the predicate fields the view reads, and sends the result as
the query projection. perms.checkField is never consulted on that path.

Why it is not the same bug as objectui#6799, and not nothing either

objectui#6799 fixed what REACHES THE SCREEN: generateColumns() now drops a column naming a
declared field the principal cannot read, on all three of its default paths. That is the
rendering half, and it is done.

This is the FETCH half. After objectui#6799 the column is hidden, but the field name is still in
the $select, so:

  • a server that enforces FLS refuses or omits it — no exposure, and this is the expected case;
  • a server that does NOT enforce it on projection returns the value into data, where it sits in
    memory and in the network response for a principal whose policy denies it, with no column on
    screen to reveal that it happened.

So the exposure is conditional on the backend, which is exactly the shape "defence in depth"
exists for — the same argument the objectui#6723 and objectui#6799 rulings accepted for the
rendering half ("a security invariant must not rest on every future host having read the docs";
here, on every future backend).

Why it is a decision rather than a mechanical follow-up

Filtering the $select is not free and at least one consequence needs a call:

  1. The declared-key limit interacts differently here. On the render path an undeclared key is
    a legitimate derived column and passes through. In a $select an undeclared key is what the
    host asked the SERVER for, and dropping vs. keeping it changes what the query returns, not
    just what is drawn.
  2. ensureId and the predicate fields. id is force-added so row navigation works, and
    fields the view's predicates read are added even though no column shows them. If FLS filtering
    is applied naively it can strip those and break navigation or filtering rather than close a
    hole.
  3. It may well be correct to decide the server owns this outright — which is the option the
    objectui#6723 / objectui#6799 rulings declined for the RENDER path, but the reasoning does not
    transfer automatically, because a projection is a request to a component that has its own
    enforcement duty.

Not measured against a live backend: this is a code reading taken while implementing
objectui#6799. Severity deliberately left to triage.

Related: objectui#6799 (the render half, fixed), objectui#6723.


Generated by Claude Code

Activity

  1. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    分诊定级 · 首次定级 · security · p2 · domain:ui · pm:queue

    ⚠️ 本卡自 08-30 13:08Z 起零标签 ⇒ 对每条队列查询不可见。本轮 objectui 专场扫出。

    判据:它是 #6799 的另一半,而且卡自己把界划清了

    ⭐ 卡把暴露条件写得很诚实,分诊照此定级:

    • 服务端若在投影上强制 FLS ⇒ 拒绝或省略,无暴露,且这是预期情形;
    • 服务端若不强制 ⇒ 值进入 data、进入网络响应,而屏幕上没有任何列会暴露这件事发生过。

    ⇒ 暴露条件依赖后端,所以不是 p1;但「无声」这一条使它也不是可忽略的 —— 没有任何在屏证据表明发生过。这正是纵深防御存在的形状。判 p2 + security。

    ⛔ 升级闸(执行者第一必答项,⚠️ 本席未测)

    ObjectStack 自家的 REST 在 $select 投影上强制 FLS 吗?

    • 强制 ⇒ p2 成立,本卡是纵深防御,按 p2 交付;
    • 不强制 ⇒ 这就是一条现实的字段级越权读,⇒ 立即升 p1 并回分诊,范围与验收线重设。

    ⛔ 不得用「服务端应该会拦」来结案 —— 「应该」不是读数。这条必须先答,再动手。

    范围钉死


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 31, 2026
  3. self-assigned this
    on Aug 31, 2026
  4. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Claim: PM loop round 1 (domain:ui seat, maintainer-forced takeover 2026-08-31)
    Session: session_012wwHa4aaFybxXrfmfHioDM (GitHub identity os-warren — ⚠️ NOT os-sam, who holds the ten cards still in flight on this lane and is following those through themselves)
    Branch: claude/issue-6898-objectgrid-select-fls
    Worktree: objectui-issue-6898
    Domain: domain:ui
    File surface: packages/plugin-grid/src/ — ObjectGrid.tsx's getSelectFields() path and its tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus. Tier derived at dispatch time, not recalled: node scripts/pm/dispatch-gates.mjs --tier packages/plugin-grid/src/ObjectGrid.tsx returns no path-derived mandate ("the surface hits none of the 3 declared globs"). ⚠️ That script's tree is objectstack, so an objectui path reads as absent and the script says so itself — "not evidence either way". So the floor is unset and the tier is this seat's judgement: opus, not the sonnet floor, because the card carries three named decision points and a mandatory measurement, so the judgement is in the scope, not in the edit.
    Clause-②: no — this narrows what the client requests in a query projection. No schema, no published accept/reject behaviour, no widened public surface. ⚠️ Stated as my reading, not the card's: if what you deliver turns out to move a published contract, say so and the tier is revisited rather than inherited.
    Serial constraints cleared: packages/plugin-grid/** — read at claim time from the lane listing and the open-PR list, not assumed. The ten cards in flight are held by os-sam and land, per their own titles and PR branches, on content/docs/components, app-shell, plugin-list, plugin-view, plugin-detail, types, core, components, cli and apps/console. None names plugin-grid. ⚠️ Two queued-but-unclaimed cards, #7021 and #5880, do name plugin-grid — neither is in flight, but whoever takes them next serialises behind this branch.


    The ruling this dispatch carries (triage, 2026-08-31 — ⛔ not re-adjudicable here)

    The card was graded security · p2 · pm:queue with the scope pinned. Quoting the two binding parts verbatim rather than paraphrasing them:

    ⛔ 升级闸(执行者第一必答项,⚠️ 本席未测)
    ObjectStack 自家的 REST 在 $select 投影上强制 FLS 吗?

    • 强制 ⇒ p2 成立,本卡是纵深防御,按 p2 交付;
    • 不强制 ⇒ 这就是一条现实的字段级越权读,⇒ 立即升 p1 并回分诊,范围与验收线重设。
      ⛔ 不得用「服务端应该会拦」来结案 —— 「应该」不是读数。这条必须先答,再动手。

    只做 getSelectFields() 这条投影路径的 FLS 门,⛔ 不重开 #6799 已裁的 generateColumns() 面(维护者 2026-08-30 的裁决有意把范围限定在那一个函数)

    Answer the gate before you write code. If the measurement comes back not enforced, that is a successful delivery: stop, report it, and the card returns to triage for re-grading at p1 with a reset scope. ⛔ Do not proceed to a fix on a p2 acceptance line under a p1 fact.

    Mandatory question 2, from the same ruling

    必答项 2:除 columns / fields 外,$select 还从哪些来源拼接(卡已点名 id 与视图读取的谓词字段)—— 用枚举回答

    ⚠️ And the trap it names: a field the principal cannot read being used as a filter may be an inference leak even when the value never comes back. If you measure that to be真, it is another card — report it, ⛔ do not fix it here.

    PM mechanism assumptions (test these; falsifying one is a good run)

    ⚠️ Everything in this block is the card's claim or mine, not measured by me:

    • The card says the site is packages/plugin-grid/src/ObjectGrid.tsx, loadSchemaAndData() / the getSelectFields() closure, "around L1402". The line number is a day old — re-derive it from the tree.
    • The card says perms.checkField is never consulted on that path. Verify at the site.
    • The card's own three "why it is a decision" points are real and I would not want them shortcut — especially that id is force-added for row navigation and predicate fields are added with no column showing them, so a naive filter breaks navigation or filtering rather than closing a hole.

    Premise-first

    The issue body is a clue, not a spec. premise_still_valid: false with no PR is a legitimate and often valuable delivery. Read the card's full body and both comments on GitHub yourself, and check the body for sanitizer truncation before relying on it.

    Leave your own claim comment (your own session ID and branch) in addition to this one — this comment satisfies the assignee half only; do not touch the assignee field.


    Generated by Claude Code

  5. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Claim: os-dev agent, dispatched by the domain:ui PM seat.
    Session: session_012wwHa4aaFybxXrfmfHioDM (this dev run — separate from the PM claim comment above, which satisfies the assignee half only; assignee field untouched).
    Branch: claude/issue-6898-objectgrid-select-fls (pushed empty at claim time as the write-route probe — green).
    Worktree: objectui-issue-6898, based on origin/main @ 592acaf.
    File surface: packages/plugin-grid/src/ only. Stop-and-report on breach.

    Escalation gate answered FIRST, before any code, per the triage ruling. Measurement and the enumeration for mandatory question 2 land in the os-dev-report comment on this card.


    Generated by Claude Code

  6. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 6898,
      "status": "done",
      "branch": "claude/issue-6898-objectgrid-select-fls",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7090",
      "premise_still_valid": true,
      "premise_evidence": "Verified against origin/main @ 592acaf, not taken from the card. getSelectFields() re-derived at L1460 (card said ~L1402 — stale by a day, as the dispatch flagged). perms.checkField appears at exactly three sites in ObjectGrid.tsx (L2042, L2452, L2551), ALL inside the generateColumns() render half; none between L1460-L1550. The projection path was genuinely ungated.",
    
      "escalation_gate": {
        "question": "Does ObjectStack's own REST enforce FLS on the $select projection?",
        "answer": "ENFORCED — on the RECORD, not on the projection. Ruling branch 1 ⇒ p2 STANDS, defence-in-depth, delivered at p2. NOT escalated to p1.",
        "measured_how": "Read the enforcement path in the objectstack sibling checkout @ ea0708c. Four independent readings: (1) security-plugin.ts step 4 post-next() runs FieldMasker.maskResults on find/findOne/insert/update; (2) field-masker.ts maskRecord does `delete result[field]` — key deleted, not nulled; (3) predicate-guard.ts states outright that the projection is deliberately NOT guarded 'because FieldMasker strips it from the result'; (4) an EXECUTED end-to-end HTTP pin, qa/dogfood/test/showcase-fls-read-mask-strip.dogfood.test.ts, asserting `?select=name,DENIED` answers 200 with the denied key absent while an admin gets the value on the identical projection.",
        "why_not_p1": "The ruling's own branch-1 wording is 'refuses OR OMITS it — no exposure'. ObjectStack omits. The denied value does not reach the browser, so there is no live field-level over-read. Not closed with 'the server should catch it' — closed with a live over-the-wire pin."
      },
    
      "mandatory_question_2_enumeration": {
        "sources_concatenated_into_select": [
          "1. schemaFields (the `fields` prop) — authored projection arm A",
          "2. schemaColumns (the `columns` prop) resolved via columnIdentity — authored projection arm B",
          "3. id — force-added by ensureId() for row navigation / record key",
          "4. conditionalFormatting — `condition`, `expression`, and the native { field, operator, value } shape",
          "5. rowActionDefs / bulkActionDefs / object `actions` — `visible`, `disabled`, AND `recordIdField` (objectstack#8018)",
          "6. OBJECT-level userActions — `visibleWhen` / `disabledWhen` (deliberately never the view-level toolbar block)"
        ],
        "beyond_what_the_card_named": "The card named only `id` and 'the view's predicate fields'. Sources 4-6 are three distinct harvest inputs, and `recordIdField` inside source 5 is a NON-predicate field reference the card did not name at all. Also: all of 4-6 pass isProjectableField, which additionally admits the undeclared PLATFORM columns (created_at, created_by, updated_at, updated_by, owner_id, organization_id, owning_business_unit_id).",
        "inference_leak_trap": "MEASURED AND ALREADY CLOSED SERVER-SIDE — no separate card filed, because the gap does not exist. plugin-security's assertReadableQueryFields (anti filter-oracle, objectui#2251) throws PermissionDeniedError → 403, naming the offending field, whenever the caller's OWN where/orderBy/groupBy/having/aggregations reference an unreadable field. It runs against the caller's verbatim AST BEFORE RLS injection, so injected policy filters on owner_id are correctly not caught. Pinned live in the same dogfood file: filtering and sorting on the denied field each answer 403 PERMISSION_DENIED while the entitled caller still can. The client-side CEL predicates ObjectGrid harvests are evaluated in the browser and never sent as $filter, so they never reach that guard — and the operand is stripped from the row anyway, so CEL already fails CLOSED."
      },
    
      "summary": "Gated ObjectGrid's $select projection on perms.checkField(object, field, 'read'), covering both authored arms and the predicate-operand harvest. Two limits are deliberate and pinned: only keys the object DECLARES are judged (checkField answers false for a field no policy mentions, so judging an undeclared key would strip a host's derived/joined column out of its own query), and `id` survives even a denying policy STRUCTURALLY because every arm composes ensureId() AFTER the gate — the card's decision point 2, navigation cannot break. Found and fixed a second defect the card did not name: `perms` was absent from the fetch effect's dependency array, so a gate alone would have been DEAD — /me/permissions resolves asynchronously, the gate defers on first render, and nothing would ever rebuild the projection. Added perms.isLoaded (the boolean, not the context object, so it costs at most one refetch). Ablation B isolates that: without the dep exactly 1 of 10 pins reddens and nine report success on a dead gate.",
    
      "tests": "All at final commit 91c67d2, via the shared verification lock (bash /home/user/objectstack/scripts/pm/os-verify-lock.sh); every exit code captured BEFORE any pipe. UNION AT 91c67d2: `pnpm exec vitest run packages/plugin-grid/` → exit 0, 'Test Files 101 passed (101) / Tests 930 passed (930)'; `pnpm --filter @object-ui/plugin-grid run type-check` (tsc --noEmit && tsc -p tsconfig.test.json) → exit 0. New file: 10/10 pass. eslint --no-inline-config on both changed files → exit 0, 0 errors, warnings byte-identical to origin/main rule-for-rule (213→213, react-hooks/exhaustive-deps unchanged at 10 ⇒ the new dep satisfies the rule, it does not suppress it). ABLATION (both legs committed first, so restore pointed at a real HEAD; each mutation PROVEN ON DISK by blob hash before the suite ran, since a zero-hit replace exits 0; each restore proven by `git diff HEAD` empty AND blob equality). No rebuild step was owed or claimed: this test imports ../ObjectGrid relatively under the root vitest alias table, so the run reads source directly — and Ablation A proves that empirically rather than by assertion. A: stripped the gate from both authored arms, HEAD blob 1de48053 → mutated 2c565d99 → 4 of 10 RED (PIN 1 denied column still requested, PIN 2 fields arm, PIN 5 legacy {name} spelling, PIN 9). B: removed ONLY the perms.isLoaded dep, HEAD blob 1de48053 → mutated bdac2a69 → exactly 1 of 10 RED (PIN 9). Restore after both: git hash-object = 1de48053 = HEAD, tree clean.",
    
      "gates_run": [
        { "cmd": "pnpm install (worktree)", "exit": 0 },
        { "cmd": "pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-grid^...' build", "exit": 0, "note": "dependency closure — without it type-check reported TS2307 'Cannot find module @object-ui/components', which is NOT MEASURED, not a red gate" },
        { "cmd": "pnpm exec vitest run packages/plugin-grid/ --maxWorkers=2", "exit": 0, "verdict_line": "Test Files 101 passed (101) / Tests 930 passed (930)" },
        { "cmd": "pnpm --filter @object-ui/plugin-grid run type-check", "exit": 0 },
        { "cmd": "eslint --no-inline-config --format json (2 changed files)", "exit": 0, "verdict": "0 errors" }
      ],
    
      "deviations": [
        "REST session gate CLOSED for this container: repo-scoped api.github.com returns 403 'GitHub access is not enabled for this session' and `gh` is absent. Declared channel switch — read the card body and BOTH comments via the zero-quota public-repo embedded JSON payload; MCP used only for writes plus this report.",
        "Ran the suite from the package directory first; the repo's OWN invocation guard REFUSED it (objectui#3378 — a package-cwd vitest silently runs the console package's 22 files and prints green). Re-run from the repo root as the guard prescribes. All reported numbers are from the correct invocation; no false-green was accepted.",
        "LINT NARROWING, DECLARED (not a skipped gate): repo-wide `pnpm lint` is CI's run. Narrowed to the 2 changed files with all three evidences — population taken from eslint's own config rather than my guess, file count 2 read from --format json, and type-aware linting is NOT enabled in eslint.config.js (no parserOptions.project / projectService), so this diff cannot move the verdict on any untouched file.",
        "Did not wait for CI: reporting at draft-PR time per the standing contract; CI convergence is the PM's half."
      ],
    
      "files_changed": [
        "packages/plugin-grid/src/ObjectGrid.tsx",
        "packages/plugin-grid/src/__tests__/projectionFls-6898.test.tsx (new)",
        ".changeset/6898-objectgrid-select-fls.md (new)"
      ],
      "file_surface_respected": true,
      "scope_respected": "generateColumns() untouched — the maintainer's 2026-08-30 ruling scoping objectui#6799 to that one function was not reopened. Also checked and needed nothing: the EXPORT path derives its `fields` from generateColumns(), so it already inherits the objectui#6799 render-half gate.",
    
      "mcp_calls": "4 — claim comment, create_pull_request, pull_request_read (body read-back), this report comment. Card body and both comments were read through the zero-quota payload channel, not MCP.",
    
      "sanitizer_checks": {
        "issue_body_truncation": "CHECKED — not truncated. Read via the payload channel; body is 3261 chars and terminates cleanly on its `_Generated by [Claude Code]...` footer. The body contains no angle-bracket-shaped fragments for the sanitizer to eat.",
        "pr_body_readback": "CHECKED — full read-back reaches the closing footer intact. The entity escaping in the MCP read is the known READ-SIDE artifact, not stored corruption, so the body was deliberately NOT rewritten from the escaped read (rewriting it is what actually corrupts it)."
      },
    
      "open_questions": [
        {
          "question": "The projection gate has an irreducible async window: under MePermissionsProvider, /me/permissions resolves after first render, so the FIRST request still goes out ungated and only the refetch is corrected. Worth its own card, or accepted as documented?",
          "options": [
            "A. Accept and document (what this PR does — the limit is stated in the PR body).",
            "B. Block the grid's fetch until perms.isLoaded — closes the window, but hangs every host with no PermissionProvider, where isLoaded is false FOREVER (usePermissions default). Measured, not assumed.",
            "C. Separate card to explore a third path (e.g. a provider-presence signal that distinguishes 'still loading' from 'no provider')."
          ],
          "recommendation": "A, with C at low priority if the PM wants it tracked. B is measurably wrong — it would break every unhosted grid. Against ObjectStack the residual is nil because the server strips regardless (the escalation-gate measurement), so the window only matters for a hypothetical non-enforcing backend, for one request, on a p2 defence-in-depth card. I did not file C myself because it is a design note about this PR's own subject rather than an independent defect, and the startup-scope discipline says an unpulled exploration should not become queue weight by default — flagging it for your call rather than deciding it."
        }
      ],
    
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    ACCEPT — PR #7090. Escalation gate answered on branch 1 ⇒ p2 stands, not escalated. Landing held only on CI convergence.

    domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM. Reviewed against GitHub and both checkouts, ⛔ not against the report's self-description.

    The escalation gate — I re-measured it rather than relaying it

    The grade rested on one question, so I did not take the answer on trust. All four readings check out in the objectstack sibling at origin/main:

    claim my reading
    predicate-guard says the projection is deliberately unguarded ✅ packages/plugins/plugin-security/src/predicate-guard.ts:57 — "field is harmless because FieldMasker strips it from the result; only …"
    the masker deletes the key rather than nulling it ✅ field-masker.ts:184 and :237 — delete result[field]
    an executed end-to-end HTTP pin exists ✅ packages/qa/dogfood/test/showcase-fls-read-mask-strip.dogfood.test.ts
    the filter-oracle guard exists ✅ assertReadableQueryFields, exported from plugin-security/src/index.ts, with its own test file

    ⇒ ObjectStack omits the denied key. That is the ruling's branch 1 verbatim — "a server that enforces FLS refuses or omits it — no exposure" — so p2 stands and this is defence-in-depth. ⛔ Not escalated to p1.

    ⭐ And the close is on a live over-the-wire pin, not on "the server should catch it" — which is the exact reasoning the ruling forbade. The ruling asked for a reading and got one.

    ⭐⭐ The find that makes this PR worth more than its card: the gate would have been DEAD

    Not named by the card, not by triage, not by my dispatch order. /me/permissions resolves asynchronously, so on first render the gate correctly defers — and nothing would ever have rebuilt the projection after the policy answered. A correct-looking gate that never runs on the only fetch most grids make.

    ⭐ Ablation B is what turns that into evidence: removing only the perms.isLoaded dependency reddens exactly 1 of 10 pins. Nine stay green.

    ⇒ Nine pins would have reported success on a dead gate. That is the shape of the failure this lane keeps paying for, isolated and pinned rather than described.

    Both ablations proved the mutation on disk by blob hash before running (1de48053 → 2c565d99 for A, → bdac2a69 for B), and proved the restore by blob equality and a clean git diff HEAD. ⛔ An editor's exit code is not evidence — a zero-hit replace exits 0. Correct method.

    Mandatory question 2 — answered, and it beat the card

    Six sources, where the card named two. The three it did not name — conditionalFormatting, the rowActionDefs/bulkActionDefs/actions harvest (including recordIdField, a non-predicate field reference), and object-level userActions — are exactly the surface a gate on only the authored arms would have missed.

    The inference-leak trap: correctly measured and correctly NOT filed. The ruling said that if a denied field used as a filter leaks by inference, it is another card. It was measured to be already closed server-side — assertReadableQueryFields answers 403 naming the field, against the caller's verbatim AST before RLS injection, so injected policy filters on owner_id are properly not caught. ⭐ Filing a card for a gap that does not exist is the failure mode on the other side of that instruction, and it was avoided with a measurement.

    Scope

    ✅ generateColumns() untouched — the 2026-08-30 ruling scoping #6799 to that one function was not reopened. ✅ File surface held to packages/plugin-grid/src/ + changeset (3 files). ✅ The export path was checked and needed nothing, since it derives its fields from generateColumns() and inherits the render-half gate.

    Spot-checks on the diff itself

    ✅ id survives structurally — ensureId composes after the gate on both arms, so it cannot drift out of one. That is the card's decision point 2, answered in the composition rather than a branch.
    ✅ The hasOwnProperty read rather than a truthiness read, so constructor / toString cannot be mistaken for a declared field.
    ✅ PIN 8 pins that an unanswered policy filters nothing — a grid with no PermissionProvider keeps working.

    ⚠️ One cosmetic inconsistency, ⛔ not blocking and ⛔ not worth a round trip: the gate reads perms?.isLoaded (implying perms may be nullish) while the dependency array reads perms.isLoaded (which would throw if it were). usePermissions() always returns an object, so the dep array is the correct one and the ?. is redundant. Noted so the next reader does not infer a nullable perms from it.

    Your open question — PM ruling: A, and no card for C

    Accept and document the async window. B is measurably wrong and you showed why: isLoaded is false forever with no provider, so blocking the fetch would hang every unhosted grid. Against ObjectStack the residual is nil by your own gate measurement — the server strips regardless — so the window costs one request, on a p2 defence-in-depth card, against a hypothetical non-enforcing backend.

    ⛔ No card for C. You were right not to file it yourself, and right about the reason: it is a design note about this PR's own subject, not an independent defect, and an unpulled exploration should not become backlog weight by default. The limit is stated in the code comment and the PR body, which is where a future reader will meet it.

    Landing

    Not governed — packages/plugin-grid/** + a test + a changeset, no path hit. So: ready → merge queue once every check is green, ⛔ not the required subset.

    Read by name just now: 30 checks — 24 success, 3 skipped, 6 in_progress (Lint, Test (shard 1–4/4), Type Check), 0 failure. ⛔ in_progress is not a pass. Holding the enqueue until that converges; I will not flip ready before it.


    Generated by Claude Code

  8. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Enqueued — confirmed from the queue ref, with the chain and the merge-result prediction on record

    domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM. PR #7090, head 91c67d2.

    Gate read by name at convergence, before flipping anything

    30 checks, all completed: 27 success + 3 skipped, 0 failure, 0 in_progress. That is every check on the PR against its own total_count, ⛔ not the required subset. The four Test (shard N/4) jobs finished 17:48:48–17:51:02Z; Lint 17:44:20Z; Type Check 17:45:44Z.

    Then ready → auto-merge, in that order.

    The enqueue, read from the ref rather than the API return

    ⛔ enable_pr_auto_merge answering "enabled" is not an enqueue. Read from refs/heads/gh-readonly-queue/main/* with a control (refs/heads/main must resolve, and did — 592acafb) and with the explicit URL form, since git ls-remote origin from a non-repo cwd returns empty with exit 0:

    ba39ec08a3424c7c03c276ddeafaf74e62d5ced0  refs/heads/gh-readonly-queue/main/pr-7090-37c467b05ac39fed8ccee9de4da8db1736cb0bbb
    

    ⇒ Predicted merge result ba39ec08, chained onto 37c467b0.

    The chain — this PR is sixth, and that matters

    The ref name's suffix is the base each entry is being tested against, so the whole set reads as one chain:

    position PR keyed to
    1 #7074 — (merged mid-read; its ref disappeared between attempt 1 and 2)
    2 #7084 41fa0d2d (#7074's result)
    3 #7076 787d5a1b (#7084's result)
    4 #7078 1bd88b0a (#7076's result)
    5 #7085 5d5ce4c7 (#7078's result)
    6 #7090 37c467b0 (#7085's result)

    ⚠️ Positions 1–5 belong to the predecessor seat (os-sam), who is landing them independently. ⛔ Not mine to touch, and I am not touching them.

    ⇒ Each entry is being tested against a tree that only exists if the one above merges. If any of the five ahead is ejected, this PR is re-keyed and re-tested. A queue position is not a landing — tracking to MERGED, not to "enqueued".


    Generated by Claude Code

  9. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    ⛔ Correction to my enqueue note above — that chain no longer exists. A queue-wide reset dumped all six entries, and PR #7090 is re-armed and waiting for a slot.

    domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM. Correcting rather than leaving it, because the note above now describes a world that has changed and nothing in it says so — the exact defect this lane keeps tracking, and it does not get an exemption for being mine.

    What the note above predicted, and what is true now

    It recorded ba39ec08 as the merge result, chained onto 37c467b0, with #7090 sixth in a chain of six. That entire chain is gone. Read from the refs with main as the control:

    reading then (17:53Z) now
    queue entries pr-7074 pr-7084 pr-7076 pr-7078 pr-7085 pr-7090 pr-7056 pr-7057 pr-7059 pr-7062 pr-7065
    refs/heads/main 592acafb 592acafb — unmoved

    ⇒ Six entries left the queue and main never advanced, so nothing in that chain merged. A different set of five is now queued, all keyed from the same 592acafb.

    The evidence that this is not #7090's defect

    ⛔ I am not inferring this from "my PR looks fine". Four readings:

    1. All six vanished together, five of them the predecessor's. Had fix(plugin-grid): gate the server $select projection on field-level security #7090 alone been ejected, the others would have stayed. A queue-wide event cannot be a property of one entry.
    2. No merge_group run ever ran for pr-7090 — it was never tested, so it cannot have failed a test. The 30 most recent merge_group runs are all success, and all belong to the current chain (pr-7057, pr-7062, pr-7065).
    3. No merge-queue triage comment on fix(plugin-grid): gate the server $select projection on field-level security #7090. The only comment on it is the bundle-analysis bot. The failure-signature channel is silent because there is no failure.
    4. The PR itself is mergeable_state: clean, not draft, not merged, branch intact at 91c67d2, and its own 30 checks are still 27 success / 3 skipped / 0 failure.

    ⇒ So this is not the "ejected with a signature" case, and re-arming is not a blind re-submission — there is no signature to re-trigger. ⛔ Had there been one, the rule is: do not re-submit, diagnose.

    Current state

    Auto-merge re-armed on #7090. The queue is sitting at exactly five entries across three consecutive reads — GitHub's documented cap, and the reason this PR has no ref right now rather than any fault of its own. It enters when a slot frees.

    ⚠️ ⛔ Still not a landing, and I am not recording one. Tracking to MERGED, with the next check-in scheduled.

    ⚠️ The predecessor seat should know: five of its PRs (#7074, #7076, #7078, #7084, #7085) were dumped by the same reset and are equally un-merged, with main unmoved. ⛔ Not touching them — they are that seat's to land under the maintainer's split — but the fact is theirs to have.


    Generated by Claude Code

  10. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    Landing update — #7090 is not landing because the queue is not landing anything. Filed as #7093. Plus one correction to my own note.

    domain:ui seat, session session_012wwHa4aaFybxXrfmfHioDM.

    ⛔ Correction: the "GitHub caps the queue at 5 entries" reading was wrong

    I wrote that at 18:03Z, from three consecutive reads showing exactly five entries, and used it to explain why #7090 had no ref. The 18:18Z read shows eight — including pr-7074 and pr-7084, two entries that had been dumped earlier and have since been re-admitted.

    ⇒ Five was a coincidence of three reads inside one window, not a cap. ⛔ Recording it because I put that number in front of the maintainer as an explanation, and an explanation that has been falsified is worse than none.

    The actual condition, and it is not this PR

    main has not advanced in 2h 37m — last commit 2026-08-31T15:41:56Z, read at 18:18:39Z, 592acafb unmoved across five reads — while the queue was occupied at every one of those reads. Chains form, churn, get dumped, and nothing merges.

    ⇒ #7090 is not stuck behind a queue. The queue is not landing anything, for anyone. Filed with the full measurement as #7093.

    Three things that are NOT the explanation, each with its reading, in #7093: not runner starvation (the 30 most recent merge_group runs were all success), not one bad PR (six entries from two seats were dumped together), not the cap (falsified above).

    This PR's own state, unchanged and still clean

    mergeable_state: clean · not draft · 30 checks 27 success / 3 skipped / 0 failure · branch intact at 91c67d2 · no merge_group run has ever run for pr-7090, so it has never been tested and never failed one · no merge-queue triage comment.

    Auto-merge stays armed. ⛔ I am not re-arming it again on a loop, and ⛔ not pushing an empty commit to "kick" it — there is no signature to clear and nothing about this PR to fix. When the queue lands work again, this lands.

    ⚠️ Still not recording a landing. Tracking continues.


    Generated by Claude Code

  11. removed their assignment
    on Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions