Skip to content

Repair the two plugin-auth admin-audit durability swallows — batch 6 of the #12981 worklist - #13592

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-12981-batch6-swallow-family
Aug 31, 2026
Merged

Repair the two plugin-auth admin-audit durability swallows — batch 6 of the #12981 worklist#13592
os-steve merged 3 commits into
mainfrom
claude/issue-12981-batch6-swallow-family

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Part of #12981

Batch 6 of the #12981 repair programme. Two tier-1 DARK durability swallows on plugin-auth's admin surface: an administrative action landed, its audit row was refused, and the endpoint answered 200 with nothing recorded anywhere. Control flow is unchanged at both sites — an admin operation must never fail over its own audit — but the refusal is no longer silent.

The population, re-measured rather than recalled

The card's "15 files" was measured at origin/main@196a6c73e and is stale by construction. Ran the repo's own instrument, scripts/measure-durability-swallow-family.mjs, self-test first so the numbers are a measurement and not a matcher that quietly stopped matching:

self-test: 4 positive controls yield members at their declared tier,
           3 negative controls yield none, 2 regression controls stay clear
before (b997272) after (15b2117)
MEMBERS (silent catch over an awaited write) 61 sites / 40 files 59 / 38
[1] DARK — the card's family, mechanically decided 10 sites / 9 files 8 / 7
[2] carries-error — inter-procedural, not decided by the census 25 / 20 25 / 20
[3] channelled — repaired by earlier batches 26 / 13 26 / 13
ADJACENT: QUIET answers (a LEVEL defect, not a silence defect) 93 95

DARK 10 → 8 and QUIET 93 → 95: the two repaired sites left the member set entirely rather than moving to channelled, because they now log. That is the declared repair — silence fixed, level deferred — and it is the accounting a reviewer can check without reading the diff.

The two sites

Both catch blocks were doing two jobs and were only right about one of them:

Both spelled catch { }. Each site now asks getSchema('sys_audit_log') — the registry that owns the answer — instead of reading the driver's error text, which would decide the same question by guessing.

file site what the silence was hiding
admin-user-endpoints.ts writeAdminAudit() sys_account is in plugin-audit's SKIP_OBJECTS, so for /admin/set-user-password its generic writer emits zero rows — the row refused here was the only record that a password was ever administratively reset
admin-import-users.ts run-level import audit action: 'import' with a null record_id is a shape plugin-audit's actionFor structurally cannot emit. The per-row create rows still land, which is what made this dangerous: the trail looked complete while who ran the import, under which password policy, and what it did in aggregate was gone

getSchema is declared optional on AdminUserDataEngine and IdentityImportEngine, so it is additive and nothing that type-checks today stops doing so. Where it is absent the site cannot measure the difference and therefore reports — an unmeasurable write must not be a silent one, and that direction is pinned by its own case.

Level: warn at both sites, silence-only — and why that is not my call

Both sinks are logger?: { warn(msg: string): void }, and both are published: index.ts carries export * from './admin-user-endpoints.js' and export * for ./admin-import-users.js. Neither declares error. Raising the level means widening a published sink shape, which the maintainer's 2026-08-30 ruling on #13398 refused as actively harmful — it would enrol every module on that type into the shrink-only check:optional-error-sink-contract population. Same finding batch 5 recorded for AuthManager.

Both sites are silence-repaired, neither is level-repaired, and the LEVEL remains #13398's question. No published sink shape is changed here.

Two census hits deliberately NOT repaired

  • auth-manager.ts:4784 verifyMcpAccessToken()a census over-collection, not a member. Its try holds no write at all: it is jwtVerify plus payload destructuring, and return null is the correct answer for an expired or wrong-audience token. The reported write=update@6456 is same-file helper resolution reaching recordSignInOutcome, which batch 5 already repaired at its own catch. Repairing it to fit the pattern would be the mirror-image defect AGENTS.md names for this rule.
  • verify/src/harness.ts:695 and plugin-sharing/src/share-link-service.ts:638 — out of this slice; the first is a verification harness whose gate answers either way, the second is usage telemetry. Neither is obviously a claim-to-persist.

Verification

Reverse-verified rather than asserted. The repair was committed first (a2fbf54), so the restore leg had a real reference point; the mutation reverted both source files to the merge base and was confirmed on disk before anything was measured — not by an editor exit code:

PRE  hashes: A=ea168649… B=cc79147d…
POST hashes: A=498d2d8d… B=9fe377f9…      (both changed)
PRE  repaired-marker 'NOT written'          : 1, 1
POST repaired-marker 'NOT written'          : 0, 0
POST swallow-marker 'audit table may not exist' : 1
MUTATION CONFIRMED ON DISK

Ablated run: 5 failed | 17 passed — and the five are exactly the five that should redden:

× a refused audit row is reported, and names the action that still succeeded
× plugin-audit UNINSTALLED stays silent, and does not attempt the write
× an engine without `getSchema` cannot measure the difference, so it REPORTS
× a refused run-level row is reported, and says the per-row trail survived
× plugin-audit UNINSTALLED stays silent, and does not attempt the write

The two "a healthy write reports nothing on this channel" cases stay green in both directions by design — they are absence-asserting controls against a seam that warns unconditionally, not pins. Restore proved by git diff HEAD empty and a clean git status. No dist leg applies: these tests import ./admin-user-endpoints.js, a relative sibling vitest resolves to src/.

All results below read from each gate's own verdict line, with exit codes captured before any pipe, on the pushed tree 15b2117:

  • pnpm --filter @objectstack/plugin-auth exec vitest run over admin-user-endpoints.test.ts, admin-import-users.test.ts, durability-swallow-repair.test.ts3 files, 81 passed
  • pnpm --filter @objectstack/plugin-auth typecheck — green
  • pnpm lint (eslint . --no-inline-config, whole repo) — green, run in full; no narrowing to declare
  • check:type-check-debt29 ledger entries re-measured, 1542 raw tsc errors, none above its recorded number. surplus: none, on the built closure the gate demands. This is the measurement that matters for the new test code: plugin-auth hides its tests from tsc, so the package typecheck reads none of them, and a green there would have been NOT MEASURED for the test file.
  • check:engine-double-contract — was red, by its own verdict line: RETAINED [update]: … now pins 2 engine double(s), ledger records 1. Coverage grew, which is the direction this ledger wants. Ratcheted with --write as instructed (one line, 1 -> 2); re-run green.
  • also green: check:where-matcher, check:objectql-double-limit, check:logger-receiver-detach, check:cross-package-test-inputs, check:query-options-erasure, check:dispatcher-error-vocabulary, check:type-check-coverage, check:dual-build-cjs-loads, check:nul-bytes, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:page-declaration-shape, check:doc-authoring, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-half-states, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal, and the check-adr-0087-registration / check-changeset-no-major / check-ci-filter-parity / check-comment-mask-adoption / check-cross-package-test-inputs / check-empty-changeset / check-keyed-text-bounds / check-plugin-teardown-shape / check-undeclared-dep-imports / release-rehearsal-clone --self-test scripts.
  • scripts/pm/check-half-states.mjsNOT MEASURED, exit 3 PREREQUISITE NOT MET: this container has no valid GitHub credential, so nothing was swept. Not a red; CI runs it with a real token.

Gate families were re-derived from the actual diff, twice: the first derivation named 29 families, and re-deriving after the ledger commit added scripts/** grew it to 41. The added families were run rather than assumed.

Note on the gate this card is about

scripts/check-durability-degradation-log-level.mjs is green over both files, before and after. That green means NOT MEASURED for these sites, never "level approved" — the gate matches callee names from an 18-entry vocabulary and ql.insert(...) is not in it. Its vocabulary is untouched here, and no entry was added to scripts/durability-degradation.baseline.json, which stays empty.


Generated by Claude Code

claude added 3 commits August 31, 2026 02:01
…` sites swallowed

Two tier-1 DARK durability swallows on plugin-auth's admin surface (#12981
batch 6): an administrative action landed, its audit row was refused, and the
endpoint answered 200 with nothing recorded anywhere. Control flow is unchanged
at both sites — an admin operation must never fail over its own audit — but the
refusal is no longer silent.

Both catches were doing two jobs and were only right about one. plugin-audit
UNINSTALLED means no sys_audit_log object, so nothing ever claimed the action
would be audited and silence is correct. A REFUSED write is the other thing
entirely, and it wore the same catch. Each site now asks
getSchema('sys_audit_log') — the registry that owns the answer — rather than
reading the driver's error text, which would decide the same question by
guessing. getSchema is declared optional on AdminUserDataEngine and
IdentityImportEngine, so it is additive; where it is absent the site cannot
measure the difference and therefore reports.

What was hiding in the silence: sys_account is in plugin-audit's SKIP_OBJECTS,
so the row refused in writeAdminAudit was the only record that a password was
administratively reset; and the run-level import row is a shape plugin-audit's
actionFor structurally cannot emit, so the per-row create rows kept the trail
looking complete while who ran the import and under which policy was gone.

Both sinks are re-exported from index.ts and declare no error, so the LEVEL
stays warn and remains #13398's question; only the SILENCE is repaired here.
Each seam is pinned, plus absence-asserting cases so a seam that warns
unconditionally cannot pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
…atch 6 pins

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
…inned)

Coverage grew, which is the direction the shrink-only ledger wants; the gate's
own verdict line asked for --write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/concepts/architecture.mdx (via getSchema (symbol))
  • content/docs/deployment/production-readiness.mdx (via sys_audit_log (literal))
  • content/docs/kernel/runtime-services/audit-service.mdx (via sys_audit_log (literal))
  • content/docs/permissions/record-view-auditing.mdx (via sys_audit_log (literal))
  • content/docs/plugins/packages.mdx (via sys_audit_log (literal))
  • content/docs/protocol/kernel/config-resolution.mdx (via sys_audit_log (literal))
  • content/docs/ui/setup-app.mdx (via sys_audit_log (literal))

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

  • content/docs/releases/index.mdx (via sys_audit_log (literal))
  • content/docs/releases/v14.mdx (via sys_audit_log (literal))
  • content/docs/releases/v17.mdx (via sys_audit_log (literal))

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
  • 1 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 29 pages)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 11 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 9c120f0308d5aed3c26f53c406054a95a631d156packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7ed3b984fd1cce8647c394467088a494e06e3c9a — the merge of head 15b211752b3c19f88b2a2b82b7a7f0b336e2817a into base 9c120f0308d5aed3c26f53c406054a95a631d156, 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 7ed3b984fd1cce8647c394467088a494e06e3c9a && git checkout 7ed3b984fd1cce8647c394467088a494e06e3c9a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9c120f0308d5aed3c26f53c406054a95a631d156 15b211752b3c19f88b2a2b82b7a7f0b336e2817a && git checkout -B drift-repro 9c120f0308d5aed3c26f53c406054a95a631d156 && git merge --no-ff 15b211752b3c19f88b2a2b82b7a7f0b336e2817a

node scripts/docs-audit/affected-docs.mjs --json 9c120f0308d5aed3c26f53c406054a95a631d156

⚠️ 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 9c120f0308d5aed3c26f53c406054a95a631d156 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

📎 转录留档 — 本 PR 依据的 #13398 裁决,原卡已不可达

domain:services PM seat(复核席),session session_016ZC5rNQj3WEet5HAmmAkMs,2026-08-31。维护者授权本次转录(本会话,逐字:「同意你的建议」)。

这不是一条新裁决,也不是本席的判断。这是一份转录,出处三件如下:

谁裁的 维护者,经总监席录裁;录裁评论作者 GitHub 账户 zhuangjianguo
何时何地 2026-08-30T12:44:11Z,issue #13398 上,comment id 5468760007(总监席第 5 场决裁批 #3,维护者逐字「同意」)
为何在此转录 ⚠️ #13398 现已无法解析。其作者账户 os-elon 的内容被 GitHub 整体屏蔽 —— 该账户所立的 issue 不可达、所发的评论从评论列表消失。裁决评论本身是 zhuangjianguo 发的、未被屏蔽,但它住在一张不可达的卡上,因此一并够不着

本席于 2026-08-30 ~12:5xZ 亲自读过该评论全文,下列为当时所读原文,⛔ 未经改写、未经节选。


转录原文(#13398 issuecomment-5468760007)

⚖️ RULED — 维护者,2026-08-30,第 5 场总监席决裁批 #3,verbatim「同意」

裁定:A 是现状定论(级别留 warn),C 是已裁的终态、本卡收窄为 C 的活载体

  • A(现状):backfillPrimaryBu 的静默已由 PR Census the catch { return null; } durability swallow family, and repair the first two members (#12981) #13397 修好(拒绝现在计数并报告)。级别暂留 warn——operator 可见损失(全被拒的回填印空白)已消除,级别只改「谁先注意到」不改「能否注意到」。census 继续点名该站点不让它被忘。
  • ⛔ B 排除:给 OptionalSharingLoggererror? 会把该类型上每个模块一次性登记进 shrink-only 的 check:optional-error-sink-contract 人口——正是那道门存在要拒绝的形状,主动有害。
  • C(终态,本卡承载):给 backfillPrimaryBu 更严的 sink({info?, warn, error?},bulk-recompute.ts 形状),报 error 合 AGENTS.md《Degradation log levels》。⚠️ 它要求在已发布形状上要 warn ⇒ 破坏传 { info, error } 的 host——这正是 2026-08-24 裁决对三个兄弟类型作为 minor 发布的同款破坏,有先例知道怎么做。⇒ 派发按 minor + 命名破坏的升级说明;条款②按内容命中(已发布 sink 形状)走复审链、档位 CONTRACT_REVIEW_TIER
  • ⛔ 本卡即 C 的活卡,不复用已 CLOSED 的 Pay down the optional-error sink ledger — 13 paid, 2 remain and both are DESIGN CALLS (was: "15 sink types") #10556

席位边界(裁不派):录裁 + 转 pm:queue。同笔摘 needs-user-decision


这条裁决为什么是本 PR 的依据

它裁的是一类,不止一张卡:当一个降级站点通过「已发布」的 sink 形状上报时,把级别抬到 error 会把该类型上每个模块一次性登记进 shrink-only 的 check:optional-error-sink-contract 人口 —— 选项 B 被判为主动有害。

本 PR 的两个站点正落在这一类里。本席复核时独立复验了那个承重事实(⛔ 不取自报告):

packages/plugins/plugin-auth/src/index.ts:19   export * from './admin-user-endpoints.js';
packages/plugins/plugin-auth/src/index.ts:24   export * from './admin-import-users.js';

两个 sink 均为 logger?: { warn(msg: string): void },均已发布,且都不声明 error。⇒ 本 PR 只修静默、级别留 warn 是在执行上述裁决,而不是本席或 dev 的自由裁量。Clause-②: no 亦由此成立 —— 没有任何已发布 sink 形状被改动。

对复审席的说明


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 31, 2026 04:13
@os-steve
os-steve enabled auto-merge August 31, 2026 04:13
@os-steve
os-steve added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit e238c79 Aug 31, 2026
34 checks passed
@os-steve
os-steve deleted the claude/issue-12981-batch6-swallow-family branch August 31, 2026 04:36
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants