Skip to content

plugin-approvals: the approver a step waits on gets no read of the record it is about — "无法打开此记录" on the request they must decide #26180

Closed

Description

@objectstack-fleet

Ruled: 6429330267 · letter A · 2026-10-26T22:59Z

Filing class: ① product defect — reach: public door, measured in a browser on 18.1.0 with objectstack-ai/hotclm main (pnpm demo:zh, personas made the README way). Needs a maintainer reading because it widens who reads a record. Reader: the approvals lane (plugin-approvals + the security read path), then objectui's request page.

Business reason

An approver is asked to decide on a record. Unless the app happens to grant them read of that exact row, they decide from the request's snapshot fields only — for a contract that means approving without opening the document, the clauses or the legal review. Every app with approvals pays this, and each would otherwise hand-write sharing rules that mirror the approval routing (a second copy of the slate the approvals service already resolves).

Measured (hotclm)

Business Requester 2 launches a 250,000 order; the ladder opens rung 1 = their manager (Executive, holds only the default member set), rung 2 = Finance Controller.

  • Executive: 待我处理 lists it; the request page shows the snapshot and the record card reads 「无法打开此记录」; /apps/clm/clm_contract/record/<id> answers 「未找到记录」. Executive approves anyway.
  • Finance Controller: the same while the contract is in_approval; after approval the record opens (the app's sharing rule starts at approved).
    The rung-1 case is structural on the open edition: the app's own_and_reports read scope fails closed to owner-only without the hierarchy resolver.

Why

The approvals service resolves the concrete slate but grants nothing; the Console hides the link when the approver cannot read the record (objectui#11878) rather than letting them read it.

Expected (for the maintainer to confirm)

While a step is pending, each approver on its slate can READ the record it is about (no edit, no children beyond what the record page reads for a reader), revoked when the step closes — or a declared flow/step switch that turns this on per approval process. The app then declares nothing.

App-side card: objectstack-ai/hotclm#134 (its option B is this card).

Dedupe: objectstack and objectui issues (2,000 most recently updated each, all states) grepped for approver … read|share|access … record, pending approver — neighbours objectui#11878 (hide the link), objectui#13324 / objectstack#25228 (blank request page), none grants a read.


Filed from the hotclm browser regression thread (2026-10-26); maintainer: 「平台的问题应该提交issue到平台」.

Activity

  1. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · security · priority:p2 · domain:server · area:workflow · needs-user-decision · pm:awaiting-maintainer · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-26T15:55Z

    ⛔ Not a claim, ⛔ not a dispatch.

    • For the maintainer: it widens who reads a record, a security boundary on the human floor. Today an approver decides from the request's snapshot alone unless the app happens to grant them read of that row.
    • Options:
      • A, scoped read for the pending approver (triage leaning). While a request is pending and the user is on the current step's slate (or holds the override), they get read of that one record through the approval, with no write. It ends when the step resolves, and explain shows it as "via approval request N". Workload: M (security read path, approvals), plus a contract review.
      • B, keep it the app's job. Document that an approver needs read. Zero code, but every app with approvals keeps paying.
      • C, a richer snapshot. The request carries more fields. That is no read widening, but it is a copy of the data that drifts, and attachments are still out of reach.
    • Lean: A. It is the narrowest grant that lets an approver see what they approve, bounded by the slate and the step.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #402 · 2026-10-26T17:39Z

    Director seat, summon #51 continuation (ledger objectstack#12708). Presented to the maintainer in batch #402 as item 1 of 2 (the two approvals cards filed from the hotclm browser regression thread). Seat increment (re-verified on origin/main cdd4eea9; the card and the triage leaning 6420598906 / 6420611091 are not restated):

    • The approvals service already asks one readability question, but a different one: filterApproversWhoCanRead (approver-org-scope.ts:221, ADR-0105 D9) drops approvers who could not read the request row across organizations; nothing grants or checks read of the record the request is about. The card's premise stands: the slate is resolved, the record stays behind the app's own sharing, and the console hides the link rather than opening it (RecordPreviewCard.tsx:59–73, objectui#11878).
    • git grep -iE 'approver.{0,60}read' on AGENTS.md + docs/adr → ADR-0105 D9 (request reads, membership union) and ADR-0142 (slate as a recipient kind); no ADR grants a record read through an approval. 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 (readers see requests when approvalsVisibleToReaders is on) is the inverse direction and untouched by A.
    • Half-state read, not written: needs-user-decision + pm:awaiting-maintainer together; the second leaves in the ruling's stroke.
    • Facets (the card has no block; seat's reading): ① A is the platform mechanism (the slate the service already resolves becomes a read scope, bounded by step and time), where B makes every app hand-write sharing rules that mirror routing and C copies data into the request; ② measured on hotclm by two personas; structural for every approver outside the record's sharing, and on the open edition for every own_and_reports manager (fails closed without the hierarchy resolver); ③ A is one grant computed by the server and shown in explain as "via approval request N"; B's app-side mirror drifts silently from the slate; ④ A adds no key — the grant is derived from state that exists — and no gate; attachments reach the approver only as the record page's reader reads them (ADR-0104 download authz inherits the parent read).
    • The seat agrees with A with its bounds stated for the ruling: read only, no edit, no children beyond what the record page reads for a reader; only while the step is pending and only for the resolved persons on the current slate (or the override holder); revoked when the step resolves; explain names the request. Workload (card column): M in the security read path + approvals, contract-review tier; Clause-②: no unless the grant is exposed as a declarable switch (then yes (widening), Metadata-protocol: added).
      Prior rulings read: filterApproversWhoCanRead · ADR-0105 D9 · #8652 · objectui#11878 → the request-read filter, the readers-see-requests ruling, the console's hidden link; thread: 6420598906.
  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #402 item 1 · letter A · maintainer 「同意」 2026-10-26T22:59Z

    Recorded by the director seat (summon #51 continuation, session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708). The maintainer answered batch #402 with 「同意」 — both seat letters. Brief 6423115398. Letter A, a scoped read for the pending approver, with its bounds settled in this stroke: while an approval request is pending, each resolved person on the current step's slate (and the override holder) may read the one record the request is about — no write, no children beyond what the record page reads for a reader (attachments only as ADR-0104's download authorization inherits the parent read); the grant exists only while the step is pending and ends when the step resolves; explain names it ("via approval request N"). The grant is derived from the slate the approvals service already resolves — no new metadata key, no app declaration; #8652 (readers see requests when approvalsVisibleToReaders is on) is the inverse direction and untouched. Not B (every app hand-writes sharing rules that mirror routing and drift from it silently) and not C (a copy of the data in the request, attachments still out of reach).

    Premises the dev verifies first (re-check in the card and the brief): filterApproversWhoCanRead (approver-org-scope.ts:221) filters on request-row readability across organizations and grants nothing on the record; the console hides the record link when the read fails (RecordPreviewCard.tsx:59–73, objectui#11878); no approval-based read exists in plugin-sharing / plugin-security today. A premise failing ⇒ a fork, ⛔ no forcing.

    Workload of the chosen letter (card column): domain:server M — the security read path consults the approvals service for the caller's pending-slate records (one read scope, time-bounded), the explain layer, integration tests for a slate member / a non-member / a member after the step resolved, and a contract review (security boundary). Clause-②: no (no authorable key; the grant is derived), Metadata-protocol: none; if the dev finds a declarable switch necessary, it comes back as a fork, not as a silent key.

    Execution: needs-user-decision → pm:queue in this act (domain:server, p2, security, area:workflow); pm:awaiting-maintainer removed in the same stroke. After it lands the console's hidden-link rule needs no change (the read now succeeds); a ui sub-card only if the request page needs one. objectstack-ai/hotclm#134 (its option B) unblocks on a published release. Ruling-ref: names this comment.

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8 · card thread
    Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_016NZtJpJqVWVJA1XDU9dbnz
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-crp41o (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#1
    Ruling-ref: 6429330267 (batch #402 item 1, letter A, maintainer 「同意」 2026-10-26T22:59Z): while an approval request is pending, each resolved person on the current step's slate (and the override holder) may read the one record the request is about. It is read only, ends when the step resolves, never crosses an organization, and explain names it ("via approval request N"). The grant is derived from the resolved slate: no new metadata key. Security card on the maintainer's word: no advisory, no new issue.
    File surface: packages/plugins/plugin-approvals/**, the read-grant seam in packages/plugins/plugin-security/** or packages/plugins/plugin-sharing/** that the dev measures as the right home, integration tests, and a patch changeset. ⛔ No ui/** (a request-page change goes to a ui sub-card the seat files). ⛔ No packages/spec/** and no new authorable key (a needed switch is a needs_decision fork).
    Container & model: S, mode:thread, default model and effort (set at thread open)
    Clause-②: no — no authorable key; the grant is derived from the slate. The dev measures it on the diff. The seat runs a CONTRACT_REVIEW_TIER review before landing (security boundary).
    Metadata-protocol: none
    Responsibility: an approver on a pending step cannot open the record they are asked to approve unless they already read it (triage 6420598906)
    Thread-read: 6429330267
    Premise: holds (read 2026-10-27T00:46Z at origin/main 6390fe52ce: card open, unassigned, pm:queue, no claim; the order lists three code premises the dev verifies before any edit)
    Serial constraints cleared: #26181 (seat 1, same round) also works in packages/plugins/plugin-approvals/. Each dev re-measures open PRs on the files it touches and runs git merge-tree against any overlap before pr_create.

  5. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 26180,
    "status": "done",
    "branch": "claude/project-thread-crp41o",
    "pr": "#26397",
    "session": "session_016NZtJpJqVWVJA1XDU9dbnz",
    "premise_still_valid": true,
    "summary": "Ruling 6429330267 letter A. plugin-approvals derives, from the pending-approver index it already keeps, the records a caller may read because a pending step waits on them: persons on the current slate (user id or account email via actingAddresses, no position/team literals) and the override holder (isOverrideActor with the active org). Both probes are fenced to the caller organization, re-read status pending, and are memoized per execution context and engine write epoch (one sys_user + one sys_approval_approver read per request, one sys_approval_request read per object, never per row; only settled answers held). plugin-sharing ORs the ids into the read filter beside record shares and fails closed (no service, throw, rejected read, on-behalf-of principal: nothing); write gates never read it. plugin-security explain attributes it as record_share via "approval request N" (admits on read, neutral on writes). No metadata key, no app declaration, no ui/**, #8652 untouched.",
    "tests": "Red on main: five source files reverted to base 6390fe5 under a HEAD-restore trap, three plugins rebuilt, ablation-dist-preflight --absent confirmed; dogfood approval-pending-approver-record-read failed at line 183 expected 404 to be 200; restore proved by empty git diff HEAD, dist rebuilt with presence preflight. Green on e8fb0d3: dogfood file 1 passed (member 200 + list, stranger 404, PATCH 403 nothing landed, explain names approval request N, overrider 200, advance revokes first and grants second, final approve and reject revoke). plugin-approvals Test Files 100/100, Tests 1286/1286 (lock VERDICT command-exit 0), incl. new pending-approver-read-grant.integration.test.ts (person by id/email, position literal none, override holder, decided requests none on same context, org wall incl platform admin and org-less caller, read-cost pin, rejected read not memoized). plugin-sharing 1240/1240 and plugin-security 4962 passed + 45 skipped at 1639ac9 (unchanged since). Typecheck of the three plugins exit 0. Repo-level census projects core 83, objectql 5, rest 280+18 skipped, runtime 947, spec 1279, types 11, all exit 0. eslint --no-inline-config on the three plugins + dogfood file exit 0.",
    "gates": "Local subset on e8fb0d3: 74 commands from dispatch-gates --commands --repo objectstack-ai/objectstack, each timeout 900, exit captured before any pipe: 73 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (no dist for untouched packages studio, client-react, embedder-openai, knowledge-memory and others) = NOT MEASURED, CI builds them. --ran reconciliation exit 0: 74 derived, 73 run, 1 NOT-MEASURED, 0 UNRUN. Earlier heads tripped check-system-context-census (isSystem read), check-tenant-audit-census (Map.delete read as an engine write) and approver-address-readers (unclassified slot site); all fixed in-diff. CI check-runs on PR 26397: in_progress at report time.",
    "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm; the new dogfood file was run on the final head (1 passed). Dogfood Regression Gate does not run on PRs.",
    "mcp_calls": "1: mcp__github__search_pull_requests (read, open-PR overlap census). No MCP writes.",
    "api_writes": "2 relay strokes beside git push: fleet-write pr_create (POST /repos/objectstack-ai/objectstack/pulls + POST /issues/26397/assignees, run 40435451999, body read back identical 8822 bytes) and this report comment (post-stamped.mjs, POST /issues/26180/comments). Each relay stroke is one POST /repos/objectstack-ai/objectstack/dispatches. git push: 5 commits on the harness branch.",
    "open_questions": [],
    "out_of_scope_findings": [
    "overlap, not a finding: open PR 26337 (ownership queues, ADR-0142) edits the same insertion points in plugin-sharing sharing-service.ts and sharing-plugin.ts and plugin-security explain-engine.ts; git merge-tree HEAD vs its head reports content conflicts in those three files, all adjacent inserts. Whichever lands second keeps both (queue disjunct plus approval ids in the id $in arm). carrier: seat 1 · noted, not filed",
    "explain uses kind record_share for the approval grant because a dedicated kind is a spec enum + ui label edit outside this card. carrier: none · noted, not filed"
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: e8fb0d3bb12fbb202af1ba9789227e8585cb055c
    Local-runs: none
    Scope: packages/plugins/plugin-approvals/src/pending-approver-read-grant.ts, packages/plugins/plugin-approvals/src/approval-service.ts, packages/plugins/plugin-sharing/src/sharing-service.ts, packages/plugins/plugin-sharing/src/sharing-plugin.ts, packages/plugins/plugin-security/src/explain-engine.ts, packages/plugins/plugin-security/src/security-plugin-explain-access.ts, packages/plugins/plugin-approvals/src/pending-approver-read-grant.integration.test.ts, packages/plugins/plugin-approvals/src/approver-address-readers.test.ts, packages/plugins/plugin-sharing/src/sharing-service.test.ts, packages/plugins/plugin-security/src/explain-engine.test.ts, packages/qa/dogfood/test/approval-pending-approver-record-read.dogfood.test.ts, .changeset/26180-pending-approver-record-read.md + packages/plugins/plugin-approvals/src/approver-address.ts (actingAddresses, slateNamesAPerson, heldSlot), packages/plugins/plugin-approvals/src/sys-approval-approver.object.ts, packages/plugins/plugin-approvals/src/approval-service.ts at the head (isOverrideActor, callerAccountEmail, syncApproverIndex, findRequestRow, REQUEST_VISIBILITY), packages/plugins/plugin-sharing/src/sharing-plugin.ts (buildSharingMiddleware, the read and write legs), packages/plugins/plugin-security/src/security-plugin-sharing-verdicts.ts (resolveSharingReadFilter), packages/plugins/plugin-security/src/security-plugin.ts (computeControlledByParentFilter), packages/plugins/plugin-approvals/src/approver-org-scope.ts (filterApproversWhoCanRead, the premise), docs/adr/0105 D9, docs/adr/0090 D10

    Judged as a security-boundary change against ruling 6429330267 (letter A) and its bounds, on the diff origin/main...pr-26397 (12 files, +1049/−2) and the cited code at the head. Gate verdicts on this head as read at review time: 20 check-runs concluded success (Build/Lint/Type Check source gates, ESLint, Check Changeset, Check PR Size, Governed Surface Queue Guard, the three claim guards, Spec property liveness, Dogfood Regression Gate, Flag docs, Auto Label, filter); 20 still in_progress (Test Core 1–8, Build Core, Type Check workspace/consumer/debt, Lint CI/engine, query-options erasure, slot lookup, docs guards, repo gates); none failed; 11 skipped (UI, docs, opt-in harnesses). The verdict below is on the diff; the seat re-reads the conclusions before landing.

    ① Derived judgments

    Accept-set changes the diff implies, each named right or wrong against the ruling:

    1. Record read (findOne), list (find), count, aggregate — RIGHT. buildSharingMiddleware composes SharingService.buildReadFilter for exactly these four verbs (sharing-plugin.ts, the READS leg); the filter ORs { id: { $in: approvalIds } } into the existing record-share arm beside the owner-match. The ids are the record_id of sys_approval_request rows with object_name = object, status = 'pending', the organization fence, and (for a slate member) id IN the requests whose sys_approval_approver row names the caller. So the list door widens to the one record per pending request and nothing else; the dogfood test pins list membership (listIds(first) equals the two pending records) and stranger []. Any other read door that composes the sharing filter (export, query, the explain's own sharing probe) serves the same ids; a door that bypasses the sharing filter is pre-existing and unchanged by this diff.
    2. No write path consults the grant — RIGHT. In the whole diff, pendingApproverReadGrants (sharing-side) is called from exactly two places: buildReadFilter and listApprovalReadGrants. canEdit, canDelete, buildWriteFilter and the middleware's write leg are untouched; sharing-service.test.ts pins canEdit/canDelete false and buildWriteFilter owner-only for a slate member who owns nothing; dogfood pins PATCH 403 with nothing landed. Explain attributes the rule effect: 'neutral' on an update and the layer outcome stays excluded.
    3. Ends when the step resolves — RIGHT. Three independent bounds, each sufficient: (a) syncApproverIndex rewrites the request's sys_approval_approver rows on every slate change and clears them with [] on every path that leaves pending (decide, final approve, reject, recall, cancel, send-back, escalation rewrite); (b) the request row is re-read with status: 'pending'; (c) the memo is a WeakMap keyed on the execution-context object and invalidated by engine.writeEpoch.current, which ObjectQL bumps on every write, a decision among them. Dogfood pins approve-and-advance (first approver 404, second 200), final approve (second and overrider 404), reject (404); the integration test pins a decided request on the same context object answering {}.
    4. Explain names it — RIGHT. explain-engine.ts pushes a record_share rule with via: "approval request N", grants: 'read', admits on read, neutral otherwise; the detail sentence names the request and the step-bound; security-plugin-explain-access.ts binds listApprovalReadGrants with the explained principal's context, so explain reads the same answer the filter ORed in (sharing-service.ts comment: "the two cannot disagree"). A throwing lookup attributes nothing (the call is wrapped in a catch that answers an empty list), pinned in explain-engine.test.ts.
    5. Derived from the slate the service already resolves, no new metadata key — RIGHT. The source reads only sys_approval_approver (the index the inbox already queries) and sys_approval_request; no spec file, no object field, no flow/step key, no app setting, nothing under packages/spec/** or ui/**. The slate is the open-time, OOO-substituted pending_approvers CSV mirrored into the index, so the delegate on the slate is the "resolved person" and reads; the absent delegator is not on the slate and does not.
    6. Override holder included — RIGHT, and bounded. isOverrideActor(context, callerOrg) (platform admin by posture/admin_full_access; tenant admin by posture/holdsTenantAdminRung) answers slate = null, and the request probe then drops the id predicate: every pending request of that object inside the caller's active organization. Dogfood pins organization_admin_no_bypass reading while pending and 404 after; the integration test pins the tenant admin's sweep.
    7. Never across an organization (ADR-0105) — RIGHT, stricter than the neighbouring reader. Both probes carry organization_id: org or, for an org-less caller, { $null: true } ($null is a where operator the engine lowers; relation-filter-lowering.ts, no-operator-object-door.ts cite it). activeOrganization reads organizationId ?? tenantId, the same read isOverrideActor and REQUEST_VISIBILITY make. The platform admin's override crosses the wall to decide and not to read: pinned. By contrast findRequestRow with a null tenant imposes no fence at all, so the new path is the narrower of the two. ADR-0105 D9's cross-org-targeted approver holds membership in the request's organization (else dropped), and reads only while acting in that organization: consistent.
    8. position:* / team:* / e-mail addresses on the slate — RIGHT. actingAddresses({ userId, email: accountEmail(userId), positions: [] }) yields only the caller's user id and the e-mail their own sys_user row carries (server-resolved, never client-supplied). A position:/team: literal matches no person, so a position holder who could DECIDE via heldSlot gets no read through it: narrower than the decide power, inside "each resolved person". An e-mail slot admits only the account carrying that e-mail. The new comparison site is classified reader in approver-address-readers.test.ts, so the equivalence gate sees it.
    9. Delegation / on-behalf-of — RIGHT. SharingService.pendingApproverReadGrants answers none when context.onBehalfOf?.userId is set; the middleware's ADR-0090 D10 delegator leg does compute the delegator's grants, but it is ANDed with the agent leg (which has none), so an agent never reaches a row through the delegator's approval. OOO substitution is judged under item 5.
    10. Other objects — RIGHT. The request probe is keyed object_name = object; a pending request on contract_sheet grants nothing on memo_sheet (pinned).
    11. Children and attachments — RIGHT, within "what the record page reads for a reader". The diff touches no child, file or attachment code. A controlled_by_parent detail derives its scope from the master's own getReadFilter (ADR-0139 leaf, computeControlledByParentFilter), which composes the sharing filter, so a detail under an approval-granted master is readable exactly as under a record share, no more. ADR-0104's download inheritance is not re-read here; the diff adds no file door.
    12. Fail closed — RIGHT. No approvals service, a getService throw, a probe with no member, a null answer, or a rejected read all answer NO_APPROVAL_GRANTS; a rejected read logs one warn and the read goes on owner-match plus shares; the read is never errored. The source side rethrows a failed read and never memoizes it (pinned). The probe is asked only after the OWD/depth/owner-field returns, so a null or deny-all filter never asks it (pinned).
    13. One bounded query per read, never per row — RIGHT. Per context-and-epoch: one sys_user read, one indexed sys_approval_approver read (the approver, organization_id index), then one sys_approval_request read per object; a caller on no slate stops after the index read for every object. Both reads are capped at 10,000 with a warn when the window is hit (narrowing direction: missing grants, never extra). The cost pin counts the engine's find calls. Note: resolveSharingReadFilter spreads a fresh context object per call, so the memo misses there and the cost repeats per call; still bounded, never per row.
    14. No caching that outlives a step — RIGHT, with one flag (③.3). WeakMap on the per-request context object plus the engine write epoch: a decision anywhere on the same engine retires every entry; nothing is held across requests.
    15. Public surface — RIGHT, additive only. ApprovalService.pendingApproverReadGrants(object, context) (the module itself is not exported from the barrel), SharingService.listApprovalReadGrants, SharingServiceOptions.approvals? and the exported SharingApprovalReadProbe type, ExplainEngineDeps.listApprovalReadGrants? optional. Structural late-binding, no package dependency added, no spec interface changed. The premise (filterApproversWhoCanRead filters request-row readability and grants nothing on the record; no approval-based read existed in plugin-sharing/plugin-security) holds on origin/main.

    Nothing wrong found; no widening beyond the ruling's bounds on any door.

    ② Semver level

    .changeset/26180-pending-approver-record-read.md declares @objectstack/plugin-approvals, @objectstack/plugin-sharing, @objectstack/plugin-security at patch, with Clause-②: no and Metadata-protocol: none line-initial; the PR body carries the same two lines. The diff publishes additive runtime members only (item 15), no authorable key, no spec change, no breaking removal: none takes patch, matches. Check Changeset on this head: success. Clause-②: no is right: the grant is derived, not declared.

    ③ Boundary flags

    1. Report 6431829080 open_questions: [] — nothing to answer.
    2. Dev flag, overlap with feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 (adjacent inserts in sharing-service.ts, sharing-plugin.ts, explain-engine.ts; git merge-tree content conflicts): not a finding against this diff; whichever lands second rebases and keeps both the queue disjunct and the approval ids. Seat watches the landing order.
    3. Dev flag, explain reuses kind record_share with via: "approval request N": accepted; a dedicated kind is a spec enum plus a ui label, outside this card's file surface (⛔ no packages/spec/**). Escalated as a note for the seat, not a fork.
    4. Reviewer flag, memo lifetime on a long-lived context: the write-epoch invalidation is per engine instance; a context object reused across many operations on a node whose engine saw no write would hold a grant that a step resolved on another node — the permissionSetMemo precedent carries the same property, HTTP contexts are per request, and the status re-read bounds any fresh computation. Noted, no action owed on this card.
    5. Reviewer flag, product observation: a unanimous/quorum voter who has voted leaves the index while the step stays pending for others, so loses the read until the step closes — the literal reading of "each resolved person on the current step's slate", narrower than it could be. Acceptance note states it; the seat may raise it with the maintainer if hotclm reports it.
    6. Org wall pinned at the service level only (dogfood boot is single): accepted, the service test covers member, platform admin and org-less caller.
    7. CI partial at review time (20 in progress, 0 failed): the seat confirms every required check green before landing; this record does not stand in for a red.

    Implemented-by: claude/project-thread-crp41o
    Reviewed-by: session_01R1q2xdtYgU6FKrZ7LgjPCC

    VERDICT: PASS

  7. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed

    PR #26397 merged at 2026-10-27T02:45Z by the merge queue as b7b04e5d8c on main (Fixes #26180; ACCEPT 6432091174).

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26397-*' 0 refs
    origin/main by content git cat-file -e origin/main:packages/plugins/plugin-approvals/src/pending-approver-read-grant.ts present
    Ancestry git merge-base --is-ancestor b7b04e5d8c origin/main holds

    State: this card closed by Fixes (completed); pm:dispatched removed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions