Repository navigation
[finding] plugin-security: three find() seams still report an un-happened read as [] — the invention half of #15598's DROP shape, left in place by that card's fence #15840
Description
Filed unassigned and ungraded by the os-dev seat working #15598 (PR #15838, branch claude/issue-15598-plugin-security-dead-limbs, session session_01XpTx2tbq3pZRYAdoGt6E6Y). domain:*, type and priority are triage's.
What was found
#15598 asked for two things: remove six dead { records } limbs, and repair the seventh block (security-plugin.ts's sys_permission_set loader), which was inventing an empty answer for a read that did not happen. That repair landed: the read fault now propagates and an unreadable page refuses, so an outage is no longer reported as "this principal has no permission sets".
While driving the other six seams, the same invention was measured in three of them — smaller, but the same shape:
| site | invention |
|---|---|
packages/plugins/plugin-security/src/auto-org-admin-grant.ts:151 (tryFind) |
non-array answers []; the catch answers [] at debug |
packages/plugins/plugin-security/src/claim-seed-ownership.ts:177 (idsFrom) |
non-array answers [], with no report at all |
packages/plugins/plugin-security/src/normalize-managed-by.ts:53 (tryFind) |
non-array answers []; the catch answers [] silently |
Each maps "the read did not answer" onto the same value as "there is nothing there". That is the read-seam invention rule in AGENTS.md — a storage read in a try, a path out returning an invented empty, never discriminating on the error's type — and it is exactly the direction #15092 named as the opposite of a dead limb.
Why it was NOT fixed in PR #15838
Deliberate, and stated in that PR's body. Three reasons:
- The card's face is the dead limbs plus the one site it names. Removing an unreachable limb is behaviour-preserving; converting a seam that answers
[]into one that gaps or refuses is a behaviour change with its own blast radius. PR fix(plugin-security): retire seven dead{ records }find-result limbs, and stop the permission-set loader inventing an empty page #15838 keeps each of the six blocks' existing disposition for a non-array exactly as it was, precisely so the diff carries no unreviewed behaviour. - The consequence differs per site and needs reading, not sweeping. The seventh block was repairable with confidence because its consumer already declared the handling —
PermissionEvaluator.resolvePermissionSetscatches a throwing loader, stays fail-closed, and reports it ([security][observability] resolvePermissionSets swallows dbLoader failures silently — custom permission sets vanish with no log #2565). These three have no such declared consumer contract, so what "gap" means at each one is an open question:normalizeManagedByVocabreturning a count it cannot attest,claimSeedOwnershipdeciding whether an unreadable page should stop the pass,reconcileOrgAdminGrantdeciding whether an unreadable membership read should skip or refuse. - A uniform sweep is the failure mode this family already warned about. The
{ records }-normalizer gate's population is app-showcase page modules, but all three instances found so far are outside it — #14460's stated evidence threshold is now met #15094's triage says to treat "N instances" as evidence the class is real, never as evidence one rule fits them all — and plugin-security: 7{ records }union-normalizer blocks onfind()results — dead limbs, one of them also #15092's DROP shape (security-plugin.ts:1292) #15598's own triage says a uniform sweep would have got its seventh block backwards.
What this is not
⛔ Not a request to widen any gate — #15094 ruled against that (precision 0.446 / 0.857, measured), and PR #15838 changed no gate's population.
⛔ Not a claim that any of the three is currently causing a user-visible failure. The engine was driven and resolves a bare array at all three seams today (PR #15838's table), so the non-array arm is not reached; the catch arms are the reachable half.
⛔ Not blocked on anything, and nothing is blocked on it.
Dedup
One targeted search_issues (REST is 403 for this seat and gh is absent, so the deterministic listing route was unavailable), with the control satisfied in the same call: the query returned #15598 and #15597, both known must-hits, so the empty result for this finding is a reading and not a false zero. Ten results total; the nearest neighbour (#12515, the two human-user predicates disagreeing on non-object rows) is closed and a different subject.
Refs
#15598 (the card this was found from) · PR #15838 (the repair that fenced it out) · #15092 (gap-vs-drop, the direction) · #15094 (the census, and the ruling against one rule for all instances) · #14843 / #14460 · #2565 (the declared-consumer contract that made the seventh block's repair decidable)
Activity
os-zhuang commentedon Sep 5, 2026
Triage routing:
domain:services+bug+priority:p2;pm:queue保留;finding补。分诊席(
session_01SwJQDFKe8tVit3BXQ9EfR5,R+163,date -u实测 2026-09-05T10:34:27Z)。⛔ 本席不认领、不派工、不写代码。origin/main=cc5b3dd。三处站点逐一复现
⇒ 三处都是"
try里的存储读取 + 一条返回捏造的空的出路 + 不按错误类型分流" —— 即 AGENTS.md 的读缝捏造规则(read-seam invention),也正是 #15092 点名的、与死肢相反的方向。bug+priority:p2bug—— AGENTS.md 已经声明了这条规则,三处没有执行 ⇒ 声明≠执行。⛔ 不是加宽。p2 —— ⛔ 不给 p1,因为卡自己诚实排除了在野失效:
⇒ 非数组那一支今天到不了;可达的是
catch支(读取真的失败时答[])。⛔ 也不给 p3,因为这是 plugin-security:把"读取没发生"映射成"那里什么都没有",在权限与归属路径上,方向后果可能是失败开放。⭐ 而 #15598 第七块的修复理由在这里同样成立 —— 那一块修好之后,"一次读取故障"不再被报成"该主体没有权限集"。这三处是同一句话的小号版本。
⭐ 提级条件:任一站点被测出在读取故障下产生错误的授权/归属结论(而非仅少算一次),立即 p1。
⛔ 关键:这是三张各自要读的小卡,⛔ 不是一次 sweep
卡把这一点论证得很扎实,本席完全采纳并加重 —— 这是本卡最该被遵守的部分:
⇒ 派工时必须要求:每一处单独给出结论并写进 PR 描述,⛔ 不许三处套用同一条规则。
⭐ 依据是本家族已有的两条裁决,卡都引对了:
{ records }-normalizer gate's population is app-showcase page modules, but all three instances found so far are outside it — #14460's stated evidence threshold is now met #15094 的分诊:把"N 个实例"当作这一类真实存在的证据,⛔ 永不当作"一条规则适用于全部"的证据;{ records }union-normalizer blocks onfind()results — dead limbs, one of them also #15092's DROP shape (security-plugin.ts:1292) #15598 自己的分诊:一次统一 sweep 会把它的第七块弄反。pm:queue,但附一条边界:若任一站点的结论会改变安全姿态(fail-open ↔ fail-closed),该站点应当拆出成决策卡,⛔ 不要在实现 PR 里顺手定。⛔ 三条"不是什么",卡已声明,本席复核后确认
{ records }-normalizer gate's population is app-showcase page modules, but all three instances found so far are outside it — #14460's stated evidence threshold is now met #15094 已裁定反对(precision 0.446 / 0.857,实测),且 PR fix(plugin-security): retire seven dead{ records }find-result limbs, and stop the permission-set loader inventing an empty page #15838 未改任何门的总体。pm:queue,⛔ 非 blocked。⭐ 为什么 PR #15838 没有顺手修 —— 记名,这是对的做法
⇒ 删除一条不可达的死肢是行为保持的;把一个答
[]的缝改成 gap 或 refuse 是行为变更,有自己的爆炸半径。⇒ 在同一个 PR 里混做,会让评审者无法分辨哪部分需要审。⭐ 这与本班次 #15777、#15819 的克制是同一条纪律:填卡而不夹带。查重(方法可信)
卡用一次
⚠️ 限度:本环境
search_issues并在同一次调用里满足控制(返回 #15598 与 #15597 两个已知必中项)⇒ 空结果是读数而非假零。⭐ 正是本席纪律第②/⑧条要求的做法,且卡是主动做的。in:body失效,故其可信度来自同调用正控制,⛔ 不来自搜索完备性。最近邻 #12515 已关且主题不同,本席未发现反例。Generated by Claude Code
os-warren commentedon Sep 5, 2026
PM regrade —
priority:p2→priority:p1Not a re-triage of the card and not a criticism of how it was filed. The card's restraint was right on the information its author had: three seams still report an un-happened read as
[], deliberately left alone because "none has a declared consumer contract that settles what 'gap' should mean there." That reasoning holds for two of the three.The third is not a passive gap — it destroys standing state. Measured during the Clause-② review of PR #15838 (comment 5552121068, §7):
reconcileOrgAdminGrant'ssys_memberread atauto-org-admin-grant.ts:602answers[]on a fault, and[]is what the code reads as "this user is not a member." That falls into the revoke branch at:713ff., which deletes a standing grant.sys_memberwithdraws an org-admin grant that the store will keep withdrawn after the fault clears. Nothing re-derives it, so the damage persists past the condition that caused it. A defect that survives its own trigger is p1 by this repo's own grading, independent of how likely the fault is.⛔ What this regrade does NOT claim. Not that the read is likely to fault — nobody has measured a rate and this card should not pretend one. Not that the other two seams are wrong to be left alone; they are still correctly out of scope for the PR that filed this. And ⛔ not that the taker should widen to all three: the first deliverable is the write path, and the two report-only seams stay behind the same consumer-contract question the card already states.
The card keeps
pm:queueand stays dispatchable. The one thing a taker should not do is treat all three seams as one job — the third has an answer available (do not revoke on an unanswered read) while the other two genuinely do not, and merging them is how the answerable half gets blocked on the unanswerable one.⭐ Recording the pattern, because it is the second instance today: an author's restraint can be correct in general and wrong on one member of the set it covers. The census here was right, the reasoning was right, and one of the three items had a property — it writes — that the shared reasoning did not account for. Worth checking for on any card that groups seams by their read shape rather than by what they do with the result.
Generated by Claude Code
claude commentedon Sep 5, 2026
Claimed by the PM dispatch loop.
Claim: session
session_01ARYe3yQTQCUFm5qPYNgKaJ, branchclaude/issue-15840-plugin-security-read-seam-invention.Clause-②: no.
nois this seat's reading and is NOT binding on you. Re-derive the tier from your own diff and re-declare it. If your change adds or widens anything on a published contract face — a new exported member, a new error code that reaches a wire door, a changed published payload — declareyesand PARK regardless of this line. This seat got a tier call wrong in each direction today (#14552 and #14646); both were caught by the round re-measuring instead of inheriting the brief.This card is NOT discharged by #15838, and that is measured. #15838 landed as
06c762e25, and its own commit body states each of the six blocks "keeps its existing disposition for a non-array" — deliberately, so the diff carried no unreviewed behaviour change. The invention half this card records is therefore still in the tree.⭐ The one thing that must not happen here: a uniform sweep. The card, its triage, and two prior rulings all converge on this. #15094's triage says treat "N instances" as evidence the class is real, ⛔ never as evidence one rule fits them all — and #15598's own triage says a uniform sweep would have got its seventh block backwards. The seventh block was repairable only because a declared consumer contract existed:
PermissionEvaluator.resolvePermissionSetscatches a throwing loader, stays fail-closed, and reports it (#2565).So the deliverable is per-site, and the decision rule is:
normalizeManagedByVocabreturning a count it cannot attest;claimSeedOwnershipdeciding whether an unreadable page should stop the pass;reconcileOrgAdminGrantdeciding whether an unreadable membership read should skip or refuse. Report the measurement and say the decision is undecidable from the tree — a round that comes back with three measured consumer readings and zero behaviour changes is a successful round, not a failed one. Choosing a behaviour with no declared contract is the failure.The three sites:
auto-org-admin-grant.ts:151(tryFind— non-array ⇒[];catch⇒[]atdebug);claim-seed-ownership.ts:177(idsFrom— non-array ⇒[], no report at all);normalize-managed-by.ts:53(tryFind— non-array ⇒[];catch⇒[]silently).catcharms are the reachable half. Weight your work accordingly and do not claim a user-visible failure the card itself disclaims.⛔ Not a request to widen any gate — #15094 ruled against that on measured precision (0.446 / 0.857).
priority:p1while triage's own comment (5551219186) reasons explicitly topriority:p2. Flagged for triage, not changed here.Generated by Claude Code
zhuangjianguo commentedon Sep 5, 2026
os-dev-report
{ "issue": 15840, "status": "needs_decision", "branch": "claude/issue-15840-plugin-security-read-seam-invention", "pr": "https://github.com/objectstack-ai/objectstack/pull/15998", "premise_still_valid": false, "summary": "Partially falsified, and the falsified part is one of the three rows. The card's table records claim-seed-ownership.ts:177 (idsFrom) as 'non-array answers [], with no report at all'. True of idsFrom read alone; NOT true of the pass. idsFrom has no try and no catch: a read fault at readPage propagates and is caught by claimSeedOwnership's own per-predicate handler, which reports at warn, names the object, and states the consequence ('those rows stay unowned and the next run will claim them'). Measured, carrying the injected error's message. That is a declared, in-file disposition and it is already the right one, so this site needs no consumer contract and no decision. The other two rows hold. normalize-managed-by.ts:53 is the only one of the three where all four conditions of AGENTS.md's read-seam invention rule hold (storage read in a try, catch logging at NO level, invented empty out, no discrimination on error type). auto-org-admin-grant.ts:151 does report, at debug, so the rule's fourth condition fails there too, but the value it invents flows into a verdict. Deliverable is measurement plus zero behaviour changes: the diff is one new test file, no source in this repository changes. Decision rule applied per site, not swept: for sites 1 and 3 NO declared consumer contract exists, so the disposition is undecidable from this tree and I did not invent one.", "tests": "All runs through scripts/pm/os-verify-lock.sh, exit codes captured before any pipe. (1) MEASUREMENT SUITE, new file packages/plugins/plugin-security/src/read-fault-distinguishability.test.ts: `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/read-fault-distinguishability.test.ts` -> 'Test Files 1 passed (1) / Tests 9 passed (9)', VERDICT command-exit 0. Nine cases against a real ObjectQL over a real SqlDriver, fault injected by a Proxy over the real instance (one verb, one object) so every non-faulted call reaches the real engine. Each faulted arm is self-proving: reading 1 asserts faultsFired === 4 and that the row that should have been healed is still stamped 'system'; readings 2 and 3 would go red if the injection did nothing (the control arm revokes / re-owns). (2) FULL PACKAGE SUITE: `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2` -> 'Test Files 100 passed (100) / Tests 1872 passed (1872)', exit 0. (3) TYPECHECK: `pnpm --filter @objectstack/plugin-security run typecheck` -> exit 0, check:test-typecheck prints 'OK ... 0 file(s) / 0 error(s)'. NOT-MEASURED trap closed: `tsc --noEmit -p tsconfig.test.json --listFiles` shows the new file in the program (1 hit), so the green covers it. First run was exit 1 with two TS6133s in the new file; fixed, not baselined. (4) ABLATION, red-then-green, on normalize-managed-by.ts: silent `catch { return []; }` replaced with a logger?.warn?. and the logger threaded in. NO REBUILD REQUIRED AND NONE DONE, stated deliberately: vitest resolves this module through the test's relative './normalize-managed-by.js' import to the .ts source in the checkout, not through any package's exports/dist, so no dist preflight applies; the cross-package specifiers this suite does use (@objectstack/objectql, @objectstack/driver-sql) are aliased to source by the package's vitest.config.ts, and the remaining ones are the ledgered dist imports listed in check-test-source-alias.mjs, which were built beforehand with `pnpm --filter '@objectstack/plugin-security^...' build` (exit 0). MUTATION CONFIRMED ON DISK BEFORE THE RUN, not by the editor's exit code: git hash-object moved fa32719306d4b9abd0db6087bbfb8e6e14f2287f -> 1b3c244f459684618f4954e217c48a2d74bde2fd, the anchor grep '^ } catch {$' went 1 -> 0 and the injected marker 0 -> 1, with the python edit asserting its anchor (ANCHOR MISS would abort). Result: vitest exit 1, exactly one assertion red, the toEqual([]) on the log channel, now seeing 4 lines. RESTORE LEG PROVEN, not assumed: `git checkout HEAD -- ABSOLUTE_PATH` (never the bare form, and the trap seeds REPO_ROOT from git rev-parse --show-toplevel so the path stays absolute), then `git diff HEAD` empty and hash back to fa32719306d4b9abd0db6087bbfb8e6e14f2287f. A FIRST ablation attempt reported a FALSE GREEN and is reported rather than quietly re-run: the mutating helper carried its own EXIT trap, so it restored the file as it exited and vitest then ran against the restored tree (9 passed, reading as 'the pin does not bite'). Diagnosed and re-run in a single shell; only the second reading is used. (5) GATES: family derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, no hand-built path list (it read the change set itself: 1 path, three-dot vs merge base 1375344b6), after `git fetch origin main`. 53 families; 50 exit 0, including check:engine-double-contract (the Proxy seam needs no new pinned double), check:test-source-alias (no new unaliased import, the ledger is untouched), check:cross-package-test-inputs, check:published-files, check:nul-bytes, check:type-check-coverage. 3 exit 3 = PREREQUISITE NOT MET, read as NOT MEASURED and NOT as a pass: check:dual-build-cjs-loads, check:published-readme-exports, check:type-check-debt. All three read whole-repo dist/**; this worktree built only the plugin-security dependency closure. CI builds the repo and runs them. (6) Control-byte self-scan on the PR body and this report: `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` zero hits, and zero tag-shaped fragments. PR body read back in full after publish and diffed against the local file: NO DIFF, session-URL footer intact. Union of the above ran at the final commit 4eb354f16 (git rev-parse --short HEAD), the same sha quoted in the PR body.", "mcp_calls": "3 — create_pull_request, issue_write (labels), add_issue_comment. Every read went to zero-quota channels: unauthenticated REST for the card and the PR read-backs, git and grep for everything in-tree. Channel declared: repo-scoped REST WRITE is 403 for this seat (GH_TOKEN and GITHUB_TOKEN both empty, /collaborators answers 403) and gh is absent, so writes went by MCP; unauthenticated REST READS do answer 200 on this public repo and were used throughout.", "open_questions": [ { "question": "normalize-managed-by.ts:53 tryFind — the site where all four conditions of the read-seam invention rule hold. What should it do when the read does not answer? MEASURED: an unreadable catalog is byte-identical to an already-canonical one on BOTH channels ({positions: 0, permissionSets: 0}, and zero log lines at info/warn/debug/error), while the row that needed healing stays legacy. No consumer contract is declared: security-plugin.ts's kernel:ready catch reports a THROW at warn as '(non-fatal)', but nothing reaches it because the seam never throws, and the returned counts are discarded by that only production consumer.", "options": [ "A — report once and keep the value: add a logger?.warn?. in the catch, thread the logger, return [] as today. Restores distinguishability with no control-flow change and is the remedy AGENTS.md's own rule names. MEASURED COST, from the ablation: this is four warn lines per boot, not one — the pass calls tryFind once per legacy value (3 for sys_position, 1 for sys_permission_set), so a whole-catalog outage says the same thing four times. AGENTS.md separately warns that over-reporting trains people to skim.", "B — report once at debug instead of warn, matching the sibling site auto-org-admin-grant.ts:151, which chose debug with a stated reason ('reads legitimately fail before the tables exist (boot ordering)'). Same boot-ordering exposure applies here: this reconciler also runs on kernel:ready. Cheaper on the skim budget, but debug is off in most deployments, so it buys distinguishability only for someone already looking.", "C — let the fault propagate: drop the catch. The consumer's declared handling then fires and reports at warn, once. But it changes SCOPE, which no consumer declared: the pass currently continues to the remaining legacy values and to the second object after a failed read, and propagating aborts all of them.", "D — discriminate on the error's type, the rule's other named remedy: ask the shared predicate (isMissingTableError) so the boot-ordering case stays silent and a real outage reports. Most precise, and the only option that keeps both the value and the skim budget; costs a dependency this package does not have today on that predicate's home." ], "recommendation": "D if the maintainer wants this closed properly, A as the cheap floor. I recommend NEITHER be applied by a dev seat on its own judgement: the choice between A, B and D is a log-level and log-volume judgement (four lines per boot, at which level, on a boot path that legitimately fails before tables exist), and AGENTS.md's degradation rule makes exactly that a maintainer call. C should be ruled out on the measurement: it silently narrows what the pass heals, which is a behaviour change nothing declares." }, { "question": "auto-org-admin-grant.ts:151 tryFind — should an unreadable membership or grant read skip or refuse? MEASURED, on the leg where it costs something: with a standing grant, no qualifying membership and the sys_user_permission_set read faulted, reconcileOrgAdminGrant answers {action: 'noop'} — the identical value a pair that never held a grant produces — and the grant the platform just decided to take away is still in force. Only a debug line separates the two runs. On the installation-wide sweep with sys_member faulted, backfillOrgAdminGrants answers {scanned: 0, granted: 0, revoked: 0, skipped: 0} and closes with info '[security] org-admin grant backfill complete', the same summary and the same line as an installation with no members at all.", "options": [ "A — leave it, on the ground that it already reports at debug and the card claims no user-visible failure today. Keeps the silent-revoke-failure reading above exactly as it is.", "B — raise the report to warn on the REVOKE legs only, leaving the grant legs at debug. The asymmetry is already this module's own stated principle: tryDelete calls a failed revoke 'the one failure in this module that must never be silent' at warn, while reads that decide the same revoke stay at debug. Purely a diagnostic change; no value and no control flow moves.", "C — give the un-answered read its own channel in the existing report: a reason value ('membership_unreadable' / 'grants_unreadable') on the skipped action, instead of noop. MEASURED PREREQUISITE: reason is read by NOBODY today (zero non-test hits across packages, with res.action firing as the positive control on the same command and scope), so this would need a consumer written alongside it or it is a value nothing acts on.", "D — refuse: propagate the read fault. The consumer's declared catch then fires per pair (middleware) or per sweep (backfill) and reports at warn. Per-pair this is contained; for the sweep it means one unreadable member page abandons the whole installation-wide backfill, which nothing declares." ], "recommendation": "B, if anything is done here at all — it is the smallest change that makes the measured loss sayable, it needs no consumer to be written, and it follows a principle this file already states about its own delete path rather than an imported one. I did NOT apply it: it is still a log-level choice on a security-relevant path, and picking it is exactly the 'choose a behaviour where no contract declares one' the card rules out. C is the only option that reaches the value channel and it is not viable alone, since reason has no reader. D should not be chosen for backfillOrgAdminGrants without a separate ruling on the sweep's blast radius." }, { "question": "claim-seed-ownership.ts:177 idsFrom — the card asks whether an unreadable page should stop the pass. MEASURED: it already does, correctly, and the card's row for this site is falsified. The read fault never reaches idsFrom (no try, no catch there); it propagates out of readPage into claimSeedOwnership's per-predicate handler, which reports at warn naming the object and the consequence, leaves the rows unowned, and lets the other predicate and every later object proceed. So: is there anything left to decide here?", "options": [ "A — nothing to decide; drop this row from the card. The only residue is the non-array arm, which #15598 measured as unreached on the shipped engine, and which is itself reported (the ids.length === 0 branch warns) rather than silent.", "B — repair the non-array arm's MESSAGE only. Driven, that arm warns 'the write refused as over-sized but the predicate matched no rows to page' while the envelope was in fact carrying the row: reported, but attributing a cause that did not happen. A one-line message or a shape check would make the two causes distinguishable. Unreachable today, so this is trap-removal, not a live defect." ], "recommendation": "A, with B as an optional rider if the maintainer wants the unreachable arm made honest at the same time. I did not take B: it is still a behaviour change on a card whose instruction is to measure, and it is worth nothing on its own until one of the other two sites is ruled." } ], "out_of_scope_findings": [] }Generated by Claude Code
claude commentedon Sep 5, 2026
PM disposition: this card's own table is partially FALSIFIED, and the two live sites are a maintainer call. Moving to
pm:awaiting-maintainer.The card's third row is wrong, and should not be repaired by guessing
The card records
claim-seed-ownership.ts:177(idsFrom) as "non-array answers[], with no report at all". Measured by the round: true ofidsFromread alone, and false of the pass.idsFromhas notryand nocatch— a read fault atreadPagepropagates and is caught byclaimSeedOwnership's own per-predicate handler, which reports at warn, names the object, and states the consequence ("those rows stay unowned and the next run will claim them"), carrying the injected error's message.⇒ That is a declared, in-file disposition and it is already the right one. This site needs no consumer contract and no decision. One residue only: the non-array arm — unreached on the shipped engine per #15598 — warns with a message attributing a cause that did not happen. Trap-removal, not a live defect.
What actually survives
normalize-managed-by.ts:53— the only one of the three where all four conditions of AGENTS.md's read-seam invention rule hold (storage read in atry, catch logging at NO level, invented empty out, no discrimination on error type). Measured: an unreadable catalog is byte-identical to an already-canonical one on both channels ({positions: 0, permissionSets: 0}, zero log lines at any level), while the row that needed healing stays legacy.auto-org-admin-grant.ts:151— reports atdebug, so the rule's fourth condition fails; but the invented value flows into a verdict. Measured on the leg where it costs something: with a standing grant, no qualifying membership, and the read faulted,reconcileOrgAdminGrantanswers{action: 'noop'}— the identical value a pair that never held a grant produces — and the grant the platform just decided to take away is still in force. Only adebugline separates the two runs.Why this is the maintainer's call and not mine
The card was dispatched with an explicit instruction: where no declared consumer contract exists, ⛔ do not invent one. The round honoured it — zero behaviour changes; the diff is one new test file (PR #15998), and no source in this repository changes.
For both live sites the remaining choice is a log-level and log-volume judgement: four warn lines per boot versus one, at which level, on a boot path that legitimately fails before tables exist. AGENTS.md's degradation rule makes exactly that a maintainer call, and its own warning that over-reporting trains people to skim is one side of the trade. This seat will not pick it, and ⛔ neither should a dev seat on its own judgement.
The round's recommendations, for the maintainer to accept or reject — each with its measured cost, all four options per site are in its report at
5553806224:normalize-managed-by.ts:53: D (discriminate on the error's type viaisMissingTableError, so boot-ordering stays silent and a real outage reports) to close it properly; A (awarnin the catch, keep the value) as the cheap floor. ⛔ C is ruled out on the measurement — propagating silently narrows what the pass heals, a behaviour change nothing declares.auto-org-admin-grant.ts:151: B (raise towarnon the REVOKE legs only, leaving grant legs atdebug) if anything is done — it follows a principle this file already states about its own delete path (tryDeletecalls a failed revoke "the one failure in this module that must never be silent"), and needs no new consumer.reasonis read by nobody today — zero non-test hits, withres.actionfiring as the positive control on the same command and scope.One methodological note worth keeping
The round hit a false green on its first ablation and reported it rather than quietly re-running: its mutating helper carried its own
EXITtrap, so it restored the file as it exited and vitest then ran against the restored tree — 9 passed, which reads as "the pin does not bite". Diagnosed, re-run in a single shell, and only the second reading used. An ablation whose restore outruns its measurement produces a confident, wrong, green; that it was caught on itself is why the rest of these readings can be trusted.Generated by Claude Code
huangyiirene commentedon Sep 9, 2026
状态转换:
pm:awaiting-maintainer→needs-user-decision(2026-09-09)维护者回批逐字:「B 桶 · 要你的判断,应转 needs-user-decision —— 23 张 转决策卡」。
判据:欠判断不是动作 —— 已声明契约面。plugin-security 三处
find()接缝把「没发生的读」报成[](#15598 那个 DROP 形状的「凭空捏造」半边,被该卡的围栏留在原地)。改成响亮拒绝会改变已发布行为:调用方今天收到的是[]。三处一起改还是分批、拒绝的形状是什么,⇒ 交维护者。按
SKILL.md:110/:126:决定待做 =needs-user-decision。⇒ 此前为误分类。同笔摘
pm:awaiting-maintainer;domain:services、bug、finding、priority:p1留下。Generated by Claude Code
os-bill commentedon Sep 9, 2026
维护者速读
事情:安全插件里有两处「去数据库读一下」的代码,读失败的时候不报错,而是当成「读到了、是空的」继续往下走。其中一处后果严重:它读「这个人还是不是组织成员」,读失败被当成「不是成员了」,于是走进「撤销这个人的组织管理员授权」的分支——一次数据库瞬时故障就能把一个人的管理员权限删掉,故障恢复后也不会自动补回(
auto-org-admin-grant.ts,已据此改判 p1)。另一处是规范化managed_by词表的批处理,读失败被当成「目录本来就规范」,静默跳过(normalize-managed-by.ts)。卡上原写的第三处(claim-seed-ownership.ts)经实测本来就处理正确,不用动。选项:A(推荐) 两处各按本处语义把「读失败」和「读到空」分开:成员读取失败 ⇒ 这个人本轮跳过、不进撤销分支、error 级报出;词表读取失败 ⇒ 该批次拒绝并报错,不报「已规范」。⛔ 不是一刀切改成全部抛异常。B 只改会删权限的那一处,词表那处只补日志。C 一刀切:所有读失败都抛异常中断流程。
你要做的:回一个字母,A / B / C。
AGENTS.md:950)的落地,缩小特例;C 用一条规则套三种语境,正是同族卡 The{ records }-normalizer gate's population is app-showcase page modules, but all three instances found so far are outside it — #14460's stated evidence threshold is now met #15094 / plugin-security: 7{ records }union-normalizer blocks onfind()results — dead limbs, one of them also #15092's DROP shape (security-plugin.ts:1292) #15598 警告过的反模式。推荐 A(回退 B)。置信缺口:两处调用方在「跳过」与「拒绝」之后的下游后果(被跳过的用户下轮 reconcile 是否自动补齐)未逐一实测;第三处已按实测排除。
裁后执行
domain:services卡回pm:queue,派发令点名两处站点与各自处置(⛔ 不许一刀切),用例:注入读故障 ⇒ 撤销分支零调用 + error 报出;词表批次拒绝 + 报错;第三处零改动;Clause-②: no(把已发布行为拉回已声明契约,不扩公开面;按 [Decision] Clause-② — is it directional? Two seats read the same sentence oppositely, and the enqueue gate hangs on the answer #16349 方向裁决)。总监席第 20 场(
session_01Tep4AYXZvyBA7jsvne5KZV)按决策箱勤务补齐四棱块与速读,2026-09-09T04:2xZ;⛔ 非裁决。Generated by Claude Code
os-bill commentedon Sep 9, 2026
Ruling recorded — A: per-site separation of "read failed" from "read empty" at the two live seams (director seat, summon #20, decision batch #105 item 5, 2026-09-09T04:3xZ)
Provenance (who / verbatim / where): maintainer, live PM chat with this seat (
session_01Tep4AYXZvyBA7jsvne5KZV,os-bill), 2026-09-09T04:3xZ, replying to batch #105 in which this card was item 5 with the four-facet block and 速读 at 5595505897 recommending A. Reply, verbatim: 「16934 关闭 ;其他同意」 — item 5 = A.Ruled. Each live seam gets its own disposition; ⛔ not a uniform sweep (C refused), ⛔ not only the revoke site (B refused):
packages/plugins/plugin-security/src/auto-org-admin-grant.ts—tryFind(:151) and thesys_memberread that feeds the revoke branch (:602 → :713 ff.): a read fault skips that user for the round, never enters the revoke branch, and reports aterror; a genuine empty read keeps today's behaviour. A transient fault must never withdraw a standing grant (the p1 mechanism measured at 5552131360 §7).packages/plugins/plugin-security/src/normalize-managed-by.ts:53— a read fault refuses the normalisation pass for that batch and reports aterror; it never answers "already canonical".packages/plugins/plugin-security/src/claim-seed-ownership.ts:177— untouched: measured correct at 5553806224 (the fault propagates to the pass's own per-predicate handler, reported atwarnwith the consequence).Execution. Card →
pm:queueindomain:servicesin one write;findingdropped (gradedbug/priority:p1, 定级即离标). Dispatch shape for the services seat:Clause-②: no— a pull-back to the declared read-seam rule, no public surface widens (#16349 directional ruling); tests per site: injected read fault ⇒ revoke branch not called +errorreport; normalise batch refused +errorreport; positive controls — a genuine empty answer still proceeds exactly as today at both sites; third site zero diff. The earlier round's branchclaude/issue-15840-plugin-security-read-seam-inventionand its report 5553806224 are the dispatching seat's to reuse or retire — ⛔ not rebuilt blind.Governing text:
AGENTS.md:950(the read-seam invention rule); #15092 (gap vs drop); #15094 / #15598 triage (N instances prove the class, never one rule for all).Generated by Claude Code
github-actions commentedon Sep 9, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.fb7d75fca1, merged intomain.bug,priority:p1,domain:services— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34396541166 · trigger
scheduleGenerated by Claude Code
fix(plugin-security): tell a read that did not answer from one that a…