fix(plugin-security): the five remaining seeder refusals reach the author - #18564
Conversation
… the author The two declared-metadata seeders refuse to write in five more places, and every one of those refusals was spelled `logger?.warn?.(...)` -- optionally chained twice, so a caller that injected no logger got no output at all. Each site now words its own consequence, stamps its own grep token and builds its own record, over ONE shared delivery rule (`reportThroughSink`). The skips themselves are unchanged (ADR-0086 D3/D4). Claude-Session: https://claude.ai/code/session_01WmBwEiWPff9JZPd5BSGNeH Co-authored-by: Claude <noreply@anthropic.com>
… logger injected Per-site pins: the token, the site-specific consequence, the unchanged skip, the injected-logger path, a lying host sink, and a class pin that reds when the doubly-optional spelling returns to either seeder as a call site. Claude-Session: https://claude.ai/code/session_01WmBwEiWPff9JZPd5BSGNeH Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmBwEiWPff9JZPd5BSGNeH Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cb570a838f1b18702ad6ececd2fe9b1d8c4e3640 && git checkout cb570a838f1b18702ad6ececd2fe9b1d8c4e3640
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 25c9a8317bab7e3eea22a1468d928641de06161e cc9c7f139b4df2b0ba85f6bc1650bb235ba19c9e && git checkout -B drift-repro 25c9a8317bab7e3eea22a1468d928641de06161e && git merge --no-ff cc9c7f139b4df2b0ba85f6bc1650bb235ba19c9e
node scripts/docs-audit/affected-docs.mjs --json 25c9a8317bab7e3eea22a1468d928641de06161e
|
Green, and deliberately parked — this PR is waiting on a contract-review-tier seat, ⛔ not on CI and ⛔ not on its author
CI — settled green, read on this head, ⛔ not from the webhook
The 5 skips are all rostered expected skips — Why it is parked
⇒ the dispatching seat is measurably below tier. Per 「条款②复核只在实测达档的席内做…未达档席 ⛔ 不审、不起该档子代理」 and 「⛔ 免复核不放行」, this seat has ⛔ not flipped ready, ⛔ not attached auto-merge and ⛔ not enqueued — and will not, on any amount of green.
What the at-tier reviewer still owes, and what is already doneOwed: the clause-② review itself, then — on PASS — strip the label from both carriers citing the record, flip ready, attach auto-merge. Already verified by this seat (⛔ re-verify anything you don't want to inherit): caliber reconciliation exact — triage's 7 occurrences vs the card's 5 sites differ only by two prose lines ( Docs Drift Check — advisory, no action, and the lane red line is intact
Generated by Claude Code |
|
⬇️ 契约复核记录 —— 由维护者授权(「18564 你可以派fable复审」)起的隔离契约复审档复核子代理渲染, Contract reviewServed-tier: ① Derived judgmentsRead from Accept set — NOT relaxed, verified site by site:
Published surface — WIDENED by 10 value exports + 3 type exports through ② Semver levelDerived: ③ Boundary flags
Implemented-by: VERDICT: FAIL 席位的转录核验(⛔ 不是记录的一部分,是采纳依据)本席对该记录的四条承重断言逐条独立复验,⛔ 不采信自述:
⇒ FAIL 成立,本席逐字采纳。 记录形制齐备(
Generated by Claude Code |
Contract review item 11: the delivery primitive is not "a refusal an author can cause" and has no second-door consumer -- the cli door has no channel for a sink printer. The package's own logSeedDurabilityFailure records the same class as "deliberately absent from the package's index.ts: an intra-package helper, not public API". The shape is ~390 raw hits across 29 package directories, so a shared delivery rule's home is a core package, not this plugin's entry. Not a breaking change: the export never shipped -- it exists only on this unmerged branch. The function does not move either; seed-refusal-diagnostics.ts imports it relatively and all five report sites keep working. Changeset stays minor (9 value + 3 type exports remain). Claude-Session: https://claude.ai/code/session_01WmBwEiWPff9JZPd5BSGNeH Co-authored-by: Claude <noreply@anthropic.com>
|
⬇️ 契约复核记录(第二轮,判 merge 后的 head)—— 由维护者授权起的隔离契约复审档复核子代理渲染, Contract reviewServed-tier: ① Derived judgmentsRead from The two head moves:
Accept set — NOT relaxed, re-verified site by site against the current merge base: Published surface — WIDENED by 9 value + 3 type exports, all measured reachable from the built entry (the prior review's 10 + 3 minus the remedied one): ② Semver levelDerived: ③ Boundary flags
Implemented-by: VERDICT: PASS 席位的转录核验(⛔ 不是记录的一部分)本席复验了本轮最承重的一条 —— 合并没有往本 PR 的 delta 里带进任何东西: ⭐ 并记一条本轮复核推翻了上一轮复核的事:上一轮 item 13 把两处 join 拼法判为 drift,本轮读代码后判 dev 对、上一轮不成立 —— 站点 2 的
Generated by Claude Code |
清标 provenance —— 条款②闸门 PASS,双载体已剥
⇒ 双载体(本 PR + 卡 #18091)同笔剥除。清的是 PASS,⛔ 不是被剥。 落地前检三条
Generated by Claude Code |
Fixes #18091
The two declared-metadata seeders refuse to write in five more places, and every one of those refusals reported through the doubly-optional
logger?.warn?.(…), so a caller that injected no logger got no output at all. Each site now words its own consequence, stamps its own grep token and builds its own record, over ONE shared delivery rule.Caliber declared: the FULL SPELLING SWEEP — and the two readings reconcile
The dispatch ruled the full sweep (every occurrence of the doubly-optional spelling in the two seeders) over the card's five refusal sites. Re-measured on current
origin/main(79a046f8), ⛔ not carried forward:grep -cbootstrap-declared-capabilities.tsbootstrap-declared-permissions.ts⭐ The triage seat's 7 reproduces exactly. The delta of 2 is not two more call sites — it is prose:
bootstrap-declared-capabilities.ts:409andbootstrap-declared-permissions.ts:313are comment lines in which #18023 and #17516 each QUOTE the spelling while explaining the defect they repaired. Once comments are excluded, the full-sweep caliber and the refusal-site caliber name the SAME five call sites on this tree. So this PR is the full sweep, and the full sweep is 5.After the change: 0 call sites in either file, and the 2 prose lines are deliberately kept — they are the historical record of why the shape exists — and are now pinned as comments-only by a class pin that reds the moment a sixth call site appears.
Control that the instrument was reading: bare
loggeroccurrences in the two files, 18 and 16.The reading this card moves (P2), measured before anything was touched
Each site driven with no logger passed, all five console channels spied, beside the two already-repaired axes as lit controls in the same harness:
Every zero is now a 1, with every counter unchanged.
upsertPackagePermissionSetis also the ADR-0086 P2 publish materializer, which passes no collector — a repair that only lit the boot loop would have left that caller exactly as mute. It is pinned separately.⛔ No skip changed
Every skip is correct under ADR-0086 D4 (a package never writes into a foreign record) and ADR-0086 D3 (a package-managed row with no
package_idmakes uninstall undefined). Each test asserts the unchanged outcome beside the new line.Why five diagnostics and not a mechanical copy
Applied, ⛔ not re-argued: the token, the record, the wording and the CONSEQUENCE are site-specific. The five consequences are genuinely different facts —
sys_capability. That three-way wording is preserved verbatim from A refused capability declaration still suppresses the back-compat derivation, so the capability exists nowhere — and an app-declared capability can never get registry provenance #4967 Part 3;One generic "declaration skipped" sentence would send the first author hunting for a broken grant that is not broken.
What IS shared, and why that is the point of the card
Exactly one thing: WHERE the line goes. This shape had been repaired instance-by-instance twice, each repair restating the same two lines at its own call site.
reportThroughSink()is now the single derivation, so a sixth site cannot re-earn the card. It also improves on both spellings it replaces:logger?.warn?.(…)bought safety against a host sink that lies about its shape with silence — the defect;logger.warn(…)would buy noise with a throw inside a seeding pass, the hazardProjectionLogger.warnrecords;typeofguard buys neither, and keeps the receiver so a class-based host logger does not throw. Pinned on both axes.Reused, ⛔ not re-derived, per the #18088 precedent:
CollisionReportSinkis imported from the sibling module rather than re-declared. ⛔reportPermissionSetNameCollisionsis not threaded with an axis parameter — PR #18088 measured that it does not generalise.?.deliberately. A pass that did its work and refused nothing must stay silent on every console channel with no sink injected — that is the discriminating control #18023 landed, and both axes now carry one. The refusal channel is the one where silence was the defect. Asserted explicitly in the class pins.New published surface
On the criterion the two existing collision diagnostics state and no wider: a refusal an author can cause has a second door by construction (
@objectstack/lint,os build/os validate), and each of these is decidable from the declaration alone with no database.CAPABILITY_PLATFORM_NAME_REFUSED+capabilityPlatformNameRefusedDiagnostic()+reportCapabilityPlatformNameRefused()+ the record typeCAPABILITY_DECLARATION_UNOWNED+capabilityDeclarationUnownedDiagnostic()+reportCapabilityDeclarationUnowned()+ the record typePERMISSION_SET_DECLARATION_UNOWNED+permissionSetDeclarationUnownedDiagnostic()+reportPermissionSetDeclarationUnowned()+ the record type⛔
reportThroughSink()is not published either. It was on this list in the first revision and the contract review was right to refuse it: it is the delivery primitive, not a refusal an author can cause, and it has no second-door consumer — the cli door records that it deliberately does not consume a sink printer. This package's ownlogSeedDurabilityFailurestates the rule for its class verbatim — "Deliberately absent from the package'sindex.ts: this is an intra-package helper, not public API". And the shape is about 390 raw hits across 29 package directories, so a shared delivery rule's home would be a core package, not this plugin's entry. The function did not move:seed-refusal-diagnostics.tsimports it relatively and all five report sites keep working. Verified off the built surface —require(dist/index.js).reportThroughSinkisundefinedand it appears in noexportstatement indist/index.d.ts, while the controlsreportCapabilityDeclarationUnowned(a function) andCAPABILITY_DECLARATION_UNOWNED(its token) both read live.⛔ The two unreadable-rows summaries are not published: an unreadable database is a runtime condition no compile-time door can raise, so they stay package-private for the reason
position_name_fold_grantdoes.Clause-②: yes — re-derived from the delivered diff (9 value + 3 type exports reachable from the published entry).
Verification
All at
cec185ad3.Tests.
pnpm --filter @objectstack/plugin-security test— 113 files / 2196 tests passed.pnpm --filter @objectstack/plugin-security typecheck— clean (tsc --noEmit,tsconfig.scripts.json, andcheck:test-typecheck: 0 files / 0 errors / 0 pinned signatures).ABLATION A1 — delivery.
reportThroughSink's body mutated back to the doubly-optional spelling. On-disk proof before reading any result: guard line count 1 → 0, mute line count 0 → 1,git diff --numstat1/2. Result — 10 failed | 46 passed, and the ten are exactly the no-logger and lying-host pins at all five sites plus the publish door:ABLATION A2 — wording. Site 1's distinguishing clauses replaced with site 2's, simulating exactly the generic-sentence regression that is forbidden here. On-disk proof:
CURATED PLATFORM capability1 → 0,The name still resolves1 → 0, the borrowed clause 0 → 1. Result — 2 failed | 35 passed, and the two are the per-site wording pins while every delivery pin stays green:Both legs restored from
HEADunder atrap … EXIT INT TERMwith absolute paths, verified by blob-hash equality and an emptygit status --porcelain— ⛔ not by a step's exit code.Gates. Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsover the tool's own change set (8 paths vs merge base79a046f8c), each exit code captured to disk before any pipe, then reconciled with--ran: 63 derived, 60 run green, 3 NOT MEASURED, 0 UNRUN.The 3 are
check:dual-build-cjs-loads,check:i18nandcheck:type-check-debt, each exiting 3 = PREREQUISITE NOT MET — all three read whole-repo build output, and the closure build (turbo run build --filter='./packages/*' --filter='./packages/*/*') was killed at 520s by this container's foreground ceiling. ⛔ Not a pass and not a finding. CI builds that closure before the same steps.check:cross-package-test-inputsexited 1 locally and is green for this diff — measured, not argued. Its single finding ispackages/cli/test/init-created-files-summary.e2e.test.tsdescendingpackages/spec/dist/, neither of which this diff touches. Control: with this full diff in the tree andpackages/spec/distmoved aside, the same gate exits 0; moved back, 1. The finding is a function of a locally-built specdist, and CI'sLint & Repo Gatesjob runspnpm install --frozen-lockfilewith no build, so that directory does not exist where the gate runs.Lint. ⛔ Not narrowed — the whole population was measured:
eslint . --no-inline-config --format json, 6804 files (the count eslint itself reported, not an estimate), 0 errors / 0 warnings.Acceptance notes
⛔ Out of scope, filed nowhere by this PR, recorded here:
packages/plugins/plugin-security/src/seed-name-lookup.ts:452reports the batched existence read's own failure throughlogger?.warn?.(…). Measured mute here: the site-3 and site-5 cases in this PR drive an unreadable database with no logger and see exactly ONE author-visible line — this card's summary — while that one stays silent; with a logger injected the same pass produces both. It is the same class, but it is outside this card's ruled caliber (the two seeders), so it is reported for the triage seat rather than ridden in. A pin in this PR asserts the count by filtering on this card's tokens rather than counting lines, precisely so that line's removal stays visible.skippedPlatform,skippedUnowned,skippedForeign,unreadable); this branch returns an all-zero outcome, so a caller reading no log still cannot tell it happened. Widening it means editingPermissionSeedOutcomeinpermission-set-projection.ts, outside this card's declared file surface. Noted, ⛔ not filed.reportPermissionSetNameCollisionsandreportCapabilityNameCollisionseach carry their ownif (logger) … else console.warn(…), whichreportThroughSinknow supersedes and which would additionally gain the lying-host guard. Routing them through it is behaviour-preserving, but both files are outside this card's declared file surface. Noted, ⛔ not filed — it is the remaining half of closing the class.Generated by Claude Code