Skip to content

fix(plugin-security): canReadObject, getReadFilter and security/explain answer the delegated business-unit read as the read door does - #31142

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-wmhsd9
Nov 7, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-wmhsd9

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fixes #30758

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:委托管理员(只持有某个业务单元子树的 adminScope)能读到的业务单元,现在 canReadObject、getReadFilter 和 security/explain 都如实回答“能读,且只能读这几个”,与读门(find/findOne/count/aggregate)一致。
  • 为什么改:读门早已按裁决放行这类读取,但三处“回答”仍说不能读,单元选择器和 explain 会拒绝委托管理员实际能读的单元(多拒,不多放)。
  • 风险与回滚:三处都复用读门同一个判断函数,canReadObject 变 true 时 getReadFilter 同步收窄到所覆盖单元,绕过中间件的门不会读到范围外单元;回滚即还原本 PR 一个提交。
  • 先看:security-plugin-read-filter.ts(收窄,最关键)、explain-engine.ts(object_crud 与记录级判定)、security-plugin-object-admission.ts(第 5 臂的补充)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-security exec vitest run src/delegated-admin-own-name-set.posture.test.ts,预期 57/57 通过,其中 plugin-security: the delegate business-unit read (#29029) is not reflected by canReadObject / security explain, and its aggregate arm has no pin #30758 新增的 24 个用例覆盖三种租户姿态。
  • 需要你做的:无,席位落地。

What changes

A delegated administrator reads the business units its admin scopes cover (ADR-0090 D12, ruling of record 6663849489). The read door admits that read beside the CRUD grant (delegatedUnitReadScope, which reached main in eb96480, the squash of #31031; #30747 itself closed unmerged). The answers outside the door did not reflect it. This PR makes them ask the door's own judgment; it does not change what the door admits.

  • delegated-admin-gate.ts: the judgment is split out as delegatedUnitReadAdmission(gate, object, operation, context, sets, delegatorSets). delegatedUnitReadScope (the door's call) now checks only that the read carries a query and hands the rest to it, so there is one judgment and no second copy. Conditions are unchanged: a read (find, findOne, count, aggregate) of sys_business_unit, own behalf, gate composed, at least one covered unit, fail closed.
  • canReadObject: arm 5 (the find grant) accepts, where no set grants, the same admission the middleware makes at step 2. Arms 3, 4 and 6 are unchanged and still run. Object-level "some rows" follows the plugin's existing precedent for every row-restricted read: canReadObject answers true and getReadFilter carries the rows (its docblock: "true never means unrestricted — the row scope is still getReadFilter's").
  • getReadFilter: where no set grants find, the same admission's confinement (an id $in filter over the covered units) is ANDed in beside RLS and controlled-by-parent, as step 3 ANDs it. Without this half, flipping canReadObject alone would have let a bypassing door read every unit of the organization (measured below).
  • security/explain: a new optional dep delegatedUnitReadScope (wired to the same judgment). object_crud grants through it only where the principal's sets do not; the rls layer's readFilter carries the confinement; the record pass reports a unit outside the scope as not visible (decidedBy: object_crud), which matters when an organization administrator explains the delegate. export, writes and on-behalf-of principals are not admitted, as at their doors.

Before, measured on this branch's base 5f00374 with the new posture tests, identical in all three postures (single, group, isolated), delegate scope over bu_east + subtree, bu_hq outside:

question read door before after
find / findOne / count / aggregate bu_east, bu_east_field same same
canReadObject('sys_business_unit') false true
units under getReadFilter all three, bu_hq included bu_east, bu_east_field
explain read: allowed / object_crud false / denies true / grants
units under explain's readFilter all three bu_east, bu_east_field
explain record bu_east / bu_hq not visible / not visible visible / not visible

Tests

One new top-level describe in the existing shared harness delegated-admin-own-name-set.posture.test.ts (real ObjectQL over SQLite with the real SecurityPlugin), eight cases per posture:

  • canReadObject admits the delegate, and getReadFilter returns exactly the read door's units.
  • security/explain read: allowed, object_crud grants, readFilter returns exactly the door's units.
  • security/explain of one unit, asked by the delegate and by its organization administrator: covered units visible, bu_hq not.
  • aggregate: inside the scope counts the covered units, a where on bu_hq counts none, and a groupBy on an FLS-unreadable field is refused by step 2.5b (forbiddenFields: ['external_ref']).
  • CONTROLs: a caller with no scope is refused by door, canReadObject and explain alike; an organization administrator's answers are unchanged; acting on behalf of another user (even one who reads every unit) admits nothing anywhere; explain still denies create and export, and canExport stays false.

The east_admin fixture set gains one field rule (sys_business_unit.external_ref unreadable) so 2.5b has a field to refuse.

Ablations: every negative pin turned red, then restored by hash

Each leg ran through scripts/ablation-replace.mjs (anchor hit x1 → x0, replacement x0 → x1), restored with git checkout HEAD -- PATH, then checked by git hash-object against the HEAD blob and an empty git diff HEAD. The test imports the plugin from src, so no build sits between the mutation and the run.

leg mutation red
A1 getReadFilter drops the unit scope 3/57: units read bu_east, bu_east_field, bu_hq
A2 explain filterParts drops the unit scope 3/57: explain read readFilter
A3 explain record pass drops the unit-scope check 3/57: organization administrator · bu_hq: expected true to be false
A4 middleware step 3 drops extra.push(delegatedUnitScope) 15/57, including the aggregate pin: ['east:1','east_field:1','hq:1']
A5 step 2.5b condition aggregate renamed away 3/57: forbiddenFields undefined (step 2.9 still refuses, without naming the field)
A6 canReadObject passes null for the delegator sets 3/57: on-behalf-of control, expected true to be false
A6b explain wiring passes null for the delegator sets 3/57: on-behalf-of control
A7 explain wiring asks find instead of the explained operation 3/57: create: expected true to be false
A8 canReadObject treats a null admission as admitted 6/57: no-scope control and on-behalf-of control

A first A6 run against a delegator holding no read of sys_business_unit stayed green: arm 6 (the delegator's own grant) already refuses it. The control now names a delegator who reads every unit, which is the case only the judgment's own on-behalf-of condition refuses; A6 and A6b are red against that version (commit 7792e80).

Acceptance notes

  • canReadObject is also asked by the engine's ReadableIds composition over a master (ADR-0139). An app object that is controlled_by_parent with sys_business_unit as its master would now let a delegate read that object's rows under the covered units, where it read none before. No platform object is such a detail (searched packages/platform-objects/src), and ADR-0055 makes reading a detail follow reading its master, so this follows the ruled read rather than widening past it. The seat should confirm this reading.
  • objectReadAdmission's other asker is unchanged: it hands no fallback, so only canReadObject gains the delegated read.
  • Out of scope per the order and untouched: the ADR-0090 D12 note and the reference_not_found comment in packages/spec/src/api/errors.zod.ts.

Verification

Local runs on the final head
  • pnpm --filter @objectstack/plugin-security test :: exit 0 — Test Files 248 passed, Tests 5820 passed, 45 skipped (before and after the merge).
  • pnpm --filter @objectstack/plugin-security typecheck :: exit 0 (test layer: 0 files, 0 errors in the debt ledger).
  • eslint (repo binary, --no-inline-config) on the 7 touched source files :: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 families for this diff; all 64 ran on head 2d9ed89 and exited 0. dispatch-gates --ran with the exit-coded record :: exit 0 — "64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). A first pass on an earlier head had four non-zero exits, all resolved before the run above: three exit 3 for missing prerequisites (a shallow clone for check-plugin-teardown-shape --self-test, the CLI build closure for check:i18n, a full pnpm build for check:dual-build-cjs-loads), and check:query-options-erasure exit 1 because the new test helpers cast three query-option bags as any; they are typed now (commit 2d9ed89).
  • After merging origin/main (two commits, neither in plugin-security; head 40c2c8a): formula, platform-objects, plugin-auth and service-settings rebuilt, then pnpm --filter @objectstack/plugin-security typecheck and test :: exit 0 (248 files, 5820 passed, 45 skipped), and the ratchet and changeset families again on 40c2c8a:
pnpm check:query-options-erasure :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:i18n :: exit 0
pnpm check:nul-bytes :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/ci/snapshot-next.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:authz-resolver :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-stale-fill :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vouch-for-reference-callers :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Generated by Claude Code

🤖 Generated with Claude Code

…in answer the delegated business-unit read as the read door does

A delegated administrator's read of the business units its admin scopes
cover was admitted by the read door only; canReadObject and security/explain
still refused it, and getReadFilter did not confine it. The three answers now
ask the door's own judgment (delegatedUnitReadAdmission), so they admit the
same read and confine it to the same units. Adds the aggregate pin.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2bzfJoq2gdSaX2sVcL
… reads every unit

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2bzfJoq2gdSaX2sVcL
…tions instead of erasing them

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2bzfJoq2gdSaX2sVcL
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 27 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authorization.mdx (via object_crud (literal, a string literal in applyRecordAttribution))
  • content/docs/permissions/explain.mdx (via object_crud (literal, a string literal in applyRecordAttribution))
  • content/docs/permissions/field-level-security.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/index.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/rls.mdx (via object_crud (literal, a string literal in applyRecordAttribution))
  • content/docs/permissions/sharing-rules.mdx (via getReadFilter (symbol, a method of class SecurityPlugin; a top-level function))
  • content/docs/permissions/system-context.mdx (via canReadObject (symbol, a method of class SecurityPlugin; a top-level function), explainAccessForCaller (symbol, a top-level function), getReadFilter (symbol, a method of class SecurityPlugin; a top-level function))
  • content/docs/plugins/packages.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/ui/forms.mdx (via SecurityPlugin (symbol, a top-level class))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/releases/v13.mdx (via object_crud (literal, a string literal in applyRecordAttribution))
  • content/docs/releases/v15.mdx (via getReadFilter (symbol, a method of class SecurityPlugin; a top-level function))
  • content/docs/releases/v17/17-5.mdx (via canReadObject (symbol, a method of class SecurityPlugin; a top-level function))
  • content/docs/releases/v17/17-6.mdx (via canReadObject (symbol, a method of class SecurityPlugin; a top-level function), getReadFilter (symbol, a method of class SecurityPlugin; a top-level function))
  • content/docs/releases/v18/18-0.mdx (via SecurityPlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 63 of 226 client-bound route-ledger rows — the other 163 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 163: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json af3451b08bc39e7b4c163f52397cffe728b3e99d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5f9e49f7f998a5faf614bc90e96e640938d8490e — the merge of head 40c2c8aba3a162e105e7f26d7a4be8ed2f10e4bd into base af3451b08bc39e7b4c163f52397cffe728b3e99d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5f9e49f7f998a5faf614bc90e96e640938d8490e && git checkout 5f9e49f7f998a5faf614bc90e96e640938d8490e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin af3451b08bc39e7b4c163f52397cffe728b3e99d 40c2c8aba3a162e105e7f26d7a4be8ed2f10e4bd && git checkout -B drift-repro af3451b08bc39e7b4c163f52397cffe728b3e99d && git merge --no-ff 40c2c8aba3a162e105e7f26d7a4be8ed2f10e4bd

node scripts/docs-audit/affected-docs.mjs --json af3451b08bc39e7b4c163f52397cffe728b3e99d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs af3451b08bc39e7b4c163f52397cffe728b3e99d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · marchtian · session_017ihL2dFb2QdZEHEJ83cR2B

维护者速读

  • 改了什么: 委托管理员只持有某个业务单元子树的管理范围时,canReadObject、getReadFilter 和 security/explain 现在都回答“能读,且只能读这几个单元”,和读门已经放行的结果一致。
  • 为什么改: 读门早已按裁决 6663849489 放行这类读取,但这三处回答还是“不能读”,单元选择器和 explain 会拒掉委托管理员实际能读的单元。这是多拒的问题,不会多放。
  • 风险与回滚: 三处共用读门的同一个判断函数。canReadObject 变成 true 的同时,getReadFilter 收窄到所覆盖的单元,所以绕过中间件的门也读不到范围外的单元(开发实测:只改前者会漏读)。回滚即 revert。
  • 先看: security-plugin-read-filter.ts 的收窄(最关键);delegated-admin-gate.ts 拆出的 delegatedUnitReadAdmission。
  • 两分钟验证: 见 PR 正文同名行。
  • 需要你做的: 无,席位落地(非受管面)。
  • 席位意见: ACCEPT。开放问题按 A 定:应用自建对象若以 sys_business_unit 为主表、读权限随主表(controlled_by_parent),委托管理员也能读到所覆盖单元下的明细。ADR-0055 规定明细读权限跟随主表;平台自带对象里没有这种明细。

席位核对

ACCEPT · Head-sha: 40c2c8aba3a162e105e7f26d7a4be8ed2f10e4bd · 2026-11-07T14:43Z

  1. Shape: draft → main, Fixes #30758; Clause-②: no / Metadata-protocol: none match the claim 6707031937. 8 files, all in packages/plugins/plugin-security/src/** plus the changeset (the claimed surface). check-governed-merges --pr 31142: NOT governed, 316 changed lines.
  2. Premise measured: delegatedUnitReadScope reached main in eb96480 (squash of fix(objectql)!: a non-system write may name, in a lookup or user field, only a record the caller can read #31031) and was applied only by the authz middleware; canReadObject and explain answered false/denies while the door served bu_east + subtree. The before column of the PR table was taken at base 5f00374 in all three tenancy postures.
  3. Gates (check-runs at this head): 48 runs, 35 success, 13 skipped, 0 failure, 0 pending. Dev's local reading: plugin-security 5820 passed after merging main; 64/64 derived gates exit 0, 0 NOT-MEASURED (report 6708113641).
  4. Accepted, read in the diff: one judgment, delegatedUnitReadAdmission. The door's delegatedUnitReadScope now only checks for a query and delegates to it, with conditions unchanged: sys_business_unit, a read operation, own behalf, gate composed, at least one covered unit, fail closed. canReadObject arm 5 takes it only where no set grants find. The ADR-0055 master-derived scope passes no ungrantedRead, so objectReadAdmission there is unchanged. getReadFilter ANDs the id $in confinement where no set grants find. Explain's object_crud grants through it only where agentCrud is false, and never for an on-behalf-of principal or an unresolved posture. Its readFilter carries the confinement, and the record pass marks a unit outside it as not visible. Pinned in the shared posture harness (24 new cases, 3 postures). There are 9 ablation legs, each red and then restored, including "flip canReadObject without getReadFilter", which reads bu_hq.
  5. Open question (thread's Q): ruled A by the seat (detail follows master per ADR-0055; B would give a second answer that disagrees with the master's read door). Nit, not blocking: getReadFilter resolves getObjectSecurityMeta before the judgment rejects a non-sys_business_unit object. That costs one extra meta read per call while the gate is composed, and the seat does not ask for a push for it. Out of scope: none filed. Skills-docs: n/a.
  6. Landing: seat lands it (pr_ready + automerge_enable via the relay). On MERGED it checks that plugin-security: the delegate business-unit read (#29029) is not reflected by canReadObject / security explain, and its aggregate arm has no pin #30758 closed and removes pm:dispatched.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 7, 2026 14:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 7, 2026 14:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 42383103865 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (3/8) — 失败步骤: Run this shard's tests

    @objectstack/runtime:test:  FAIL   local  src/domains/mcp-object-schema-mask.test.ts > [ADR-0106 D1] a standalone or object-less action entry is served as the object door projects it — /meta action do
      ↳ 失败原因: @objectstack/runtime:test: AssertionError: usr_system item st_ping: expected '{"status":200,"body":{"type":"action"…' to be '{"status":200,"body":{"type":"action"…' // Object.is equality
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/domains/mcp-object-schema-mask.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · marchtian · session_017ihL2dFb2QdZEHEJ83cR2B

Queue red triaged · 2026-11-07T15:07Z

Queue build 42383103865 failed in Test Core (3/8), packages/runtime/src/domains/mcp-object-schema-mask.test.ts:801 (the "exempt author and system context answered byte-identical" CONTROL). The seat diffed the job log's Expected and Received strings field by field. The only difference is the response's Last-Modified header: 14:49:31 GMT expected, 14:49:30 GMT received. Two requests that straddle a second boundary produce exactly this. The assertion measures the experiment's own timing precondition, not product behaviour. That is the time-measurement class the triage comment above allows one requeue for. This PR does not touch packages/runtime, and its change cannot move a Last-Modified stamp. Re-queued once at the same head 40c2c8aba3. If it goes red a second time, the seat files a flake card for that test instead of requeueing.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 7, 2026
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 7, 2026
Merged via the queue into main with commit 449c858 Nov 7, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-wmhsd9 branch November 7, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-security: the delegate business-unit read (#29029) is not reflected by canReadObject / security explain, and its aggregate arm has no pin

2 participants