Skip to content

Repair the seven plugin-security durability swallows — batch 2 of the #12981 worklist - #13414

Merged
os-elon merged 5 commits into
mainfrom
claude/issue-12981-swallow-batch-2-plugin-security
Aug 30, 2026
Merged

Repair the seven plugin-security durability swallows — batch 2 of the #12981 worklist#13414
os-elon merged 5 commits into
mainfrom
claude/issue-12981-swallow-batch-2-plugin-security

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Part of #12981

⛔ Deliberately NOT a closing keyword. #12981 is a WORKLIST card and the ruling's own shape is "a mechanical census … then batched repairs". This is batch 2: it repairs 7 of the tail's tier-1 sites and leaves 21 enumerated below. Closing the card on merge would orphan that tail.

Batch 1 (35202f17f) landed the census and repaired two members. It fenced the seven plugin-security sites off because PR #13176 was in flight on this package's tsc program; that fence is discharged (#13395 merged as a68c61267), and batch 1 named these the highest-value members left.


1. Assumption ①, measured FIRST — the seven are still exactly seven

⛔ Not taken from batch 1's table. The instrument batch 1 landed for this purpose was re-run, and it confirms the seven by name:

[1] DARK members, by file — the repair worklist:
2× packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts
1× packages/plugins/plugin-security/src/cleanup-package-permissions.ts
4× packages/plugins/plugin-security/src/suggested-audience-bindings.ts

Tier 1 stood at 28 sites in 14 files before this PR — unchanged since batch 1, so a68c61267 moved nothing.

Assumption ④ answered: four DISTINCT sites, not one helper reached four times.suggested-audience-bindings.ts splits 3 + 1 across two functions — syncAudienceBindingSuggestions at the confirm-observed UPDATE, the create INSERT and the prune DELETE, and reapOrganizationLessSuggestions at its own DELETE. The count is not misleading, and the three inside one function share one accumulator while the reap gets its own.

The measured result

 before → after
[1] DARK 28 sites / 14 files → 21 sites / 11 files
[3] channelled 19 sites / 10 files → 26 sites / 13 files
MEMBERS 72 sites / 42 files → 72 sites / 42 files (unchanged — nothing was hidden)

Exactly +7 channelled, −7 dark, and plugin-security no longer appears in the DARK list at all (grep count 0). The members total is deliberately unchanged: the catch blocks still exist and still absorb, which is the behaviour that must not change — what moved is that the error now reaches a channel.

2. Assumption ②, confirmed rather than assumed — the accumulator IS reachable in-package

createSeedWriteRefusals / logSeedDurabilityFailure live in plugin-security/src/per-organization-catalog.ts and are absent from this package's index.ts (measured: 0 occurrences), which is exactly why they are available to these seven and were not to plugin-auth. Verified there is no import cycle: per-organization-catalog.ts imports only @objectstack/spec/security and @objectstack/types, nothing local.

⭐ The ACCUMULATOR is reused; the shared REPORTER is not. reportSeedWriteRefusals' prose is catalog-seed-specific and names an os migrate remedy for the legacy platform-wide index — untrue of all three files here. Each site gets its own report, the deviation permission-set-drift.ts already records.

3. Assumption ③ — all seven judged genuinely DURABILITY, and the judgement is applied per-site

Applying batch 1's share-link-service.ts test (a use_count telemetry stamp is functional, not durability, and escalating it is the over-application AGENTS.md forbids), nothing is judged OUT — but one site is judged out in part, which is the sharper result:

suggested-audience-bindings.ts's insert site filed EVERY failure under one documented cause. Its comment read catch { /* unique-index race with a concurrent sync — benign */ }. That cause is real and genuinely benign — the other pass created the row this one wanted — but a store outage, a missing table and a rejected column all reached the same catch and were all recorded as the benign race. The shared accumulator classifies with the shipped isUniqueViolationError predicate, so the race stays benign and is excluded from refused, while everything else is counted and reported. That is the accumulator earning its keep instead of a blanket escalation.

4. ⚠️ The seven split into TWO shapes — reported rather than made to look uniform

The split is not aesthetic; it is forced by a gate, and it decides the LEVEL each report can use.

scripts/check-optional-error-sink-contract.mjs rules that a sink type declaring an optional error must declare warn NON-optional — "an optional error with no declared alternative is a contract that permits silence". So adding error? to a sink whose warn is optional is red on arrival unless warn is made required too.

filesinkshape
bootstrap-system-capabilities.ts ×2SeedOptions.logger, and bootstrapSystemCapabilities is absent from index.ts — package-privateA — durability channel. The sink now reuses the in-package SeedLogger (required warn, optional error), so the report goes to error with the mandatory warn fallback. No new sink type enters the gate's population.
cleanup-package-permissions.ts ×1, suggested-audience-bindings.ts ×4inline literal in an exported signature, and SuggestionDeps — both exported from index.ts with warn optionalB — silence only, stays warn. Requiring warn on a published shape is a contract call above this repair.

What is fixed in shape B is the SILENCE, which needed no contract at all; the LEVEL is #12981's — the same split, for the same reason, that batch 1 recorded for plugin-sharing's backfillPrimaryBu. check:optional-error-sink is green and still reports 1 baselined, shrink-only: had shape B taken the error?, that number would have risen.

5. What each repair does

Common shape, copied from the three landed exemplars — count refusals, report once with consequence AND remedy, widen the > 0 suppressor.

  • bootstrap-system-capabilities.ts — a refused insert on the derived half fell through else if (!isDerived) into total silence, and a refused update was counted nowhere and logged nowhere on either half, while the boot went on logging [security] system capabilities seeded at info over zero landed rows. That is the card's shape verbatim. ⚠️ The report states what is actually lost and refuses to overclaim: sys_capability is the registry Setup lists, and grants resolve capabilities by name, not by row, so no principal gained or lost access — what a refused insert leaves is a capability absent installation-wide, and a refused update leaves a row whose label and description stay drifted. The curated half's existing per-name blockedCurated warning is untouched and is not double-counted; this is the pass-level total and the only report the other two paths had.
  • cleanup-package-permissions.ts — ADR-0090 D5 promises that uninstalling a package "revokes it everywhere at once. No ghost grants." That promise is an ABSENCE, and an absence cannot be read off a count of successes: { sets: 0, positionBindings: 0, userGrants: 0, suggestions: 0 } was returned both by an uninstall of a package that granted nothing and by one whose every revocation was refused — while the package door answered success: true and every surviving sys_position_permission_set / sys_user_permission_set row kept granting the uninstalled package's permissions. The old comment read "count reflects reality"; it was true of the count and false of what a reader does with it.
  • suggested-audience-bindings.ts ×4 — refused create / confirm / prune / reap, each with its own consequence in the line (no prompt at all; a nag for a decision already made; a prompt for a declaration that is gone; organization-less rows that stay readable by every tenant and suppress each tenant's own row).

CapabilitySeedResult, PackagePermissionCleanupOutcome and SuggestionSyncOutcome gain a refused count. Additive — they are returned by functions, not constructed by callers (measured: 0 references to any of the three outside this package).

6. ⭐ One file outside the surface, declared on #12981 BEFORE editing

scripts/measure-durability-swallow-family.mjs. Repairing the seven turned the instrument's own--self-test red:

x positive control .../bootstrap-system-capabilities.ts yielded 2 member(s)
but none at tier `dark` (saw: channelled).

Its dark control named one of the seven, with a why reading "the card's shape, verbatim, still standing" — a sentence this batch made false on purpose.

The general fact: any tier-1 DARK member of the worklist is a control the repair programme is designed to destroy. Repointing at another worklist member only moves the breakage to whichever batch repairs that one. The control now names plugin-sharing's share-link-service.ts, which batch 1 judged OUT on the merits (a telemetry stamp), so it is a genuine dark member with a recorded reason to stay one — verified by reading the code, not off the output. Control target only: no predicate, tier or vocabulary change, and the instrument is wired into no workflow (0 references in .github/ and root package.json). --self-test is green: 4 positive, 3 negative, 1 regression control.

7. ⛔ Ruling fences — held, and measured against a positive control

Against a diff that touches 8 files:

0 scripts/durability-degradation.baseline.json (stays `entries: []`)
0 scripts/check-durability-degradation-log-level.mjs (untouched in EITHER direction)
0 content/docs/releases/
0 packages/plugins/plugin-security/src/permission-set-projection.ts (#12020)
0 packages/plugins/plugin-security/src/packaged-permission-set-restore-leg.test.ts (#12020)
0 packages/plugins/plugin-security/tsconfig.test.json (#13395)
0 packages/plugins/plugin-security/package.json (#13395)
0 packages/runtime/src/explain-engine.ts (#11971)
0 packages/services/service-automation/src/plugin.ts (#11971)

No catch { return null; } became catch {} and nothing swallows deeper — every catch still absorbs and still never throws, which is the behaviour that must not change. No TEST_DEBT/DEBT entry raised.

8. Reverse verification — direction predicted BEFORE the run

Prediction: removing the single refusals?.record(object, e) in deleteRows turns the cleanup pins RED — refused reads 0, the refused pass and the nothing-to-do pass become equal again, and the report vanishes.

Mutation proven on disk before anything was read, restore trapped on EXIT INT TERM against an absolute path:

HEAD blob 1ea95a9bb02be66290f3264ebf67b61dd1db7698
before 1ea95a9bb02be66290f3264ebf67b61dd1db7698 (tree confirmed at HEAD first)
guard count 1 → 0 marker 'ABLATED_12981' 0 → 1
after 2cbd40ab724cf8ca5ea22eb2721a3115aa6c0b0d (≠ before, so the edit landed)

Observed: exactly the predicted direction.ABLATED_EXIT=1, 3 failed / 5 passed — the three #12981 pins red and every pre-existing test still green, so the ablation is targeted rather than a blanket break.

Restore proven by state, not by exit code: blob back to 1ea95a9b…, marker count 0, guard count 1, git diff HEAD empty, git status clean. No dist/ participates: the mutated file and its test are in the same package and the test imports it by relative path, so vitest resolves it from source — there is no rebuild leg to prove for this one.

9. Verification — final head dfbecbb4d

  • Gate family derived at edit time, never a copied list: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack. The first derivation warned STALE TREE; origin/main was merged and it was re-derived clean. Re-derived a third time after §6 added a scripts/** path, which pulled in 7 further families (agent-test-spelling, bash32-floor, cli-command-ids, entry-guard, parse-guard, pnpm-filter-targets, watch-hint-literal) — all run, all green.
  • plugin-security: typecheck + vitest run89 test files / 1651 tests passed, re-run on the merged head. ⭐ Since a68c61267 the typecheck script runs three programs including tsconfig.test.json, so this green does cover the test code this PR adds — no separate --listFiles measurement is owed, and there was nowhere to park an error if one existed.
  • Whole-workspace build, the downstream direction: turbo run build --filter=./packages/* --filter=./packages/*/*70 successful, 70 total. (An earlier --filter '...@objectstack/plugin-security' run failed on @objectstack/rest declarations; that is the dependents-only filter not selecting the dependents' own dependencies, not a red from this change.)
  • Full-repo pnpm lint (eslint . --no-inline-config), not a narrowed scan — exit 0, re-run on the final head because eslint . covers scripts/.
  • check:type-check-debt --re-measure29 ledger entries re-measured, 1547 raw tsc errors, none above its recorded number; surplus: none.
  • 30+ families run locally, all green, including the ones this change most plausibly moves: check:optional-error-sink, check:logger-receiver-detach, check:engine-double-contract, check:where-matcher, check:type-check-coverage, check:changeset-gate-self-tests.
  • ⚠️NOT MEASURED, and neither a red nor a finding:check:optional-error-sink-contract exited 254 — that is the wrong script name (the script is check:optional-error-sink), so nothing ran. Re-run under its real name: green. check:i18n / check:i18n-stale-fill are convention-triggered on packages owning an i18n-extract.config.ts; plugin-security has none, so this change cannot move them.
  • Exit codes captured before any pipe throughout (cmd > log 2>&1; EXIT=$?), and every verdict quoted is the gate's own printed line, never a bare $?.

⚠️check:durability-log-level is green over all eight files, and per this card's standing premise that green is NOT MEASURED for every site named here — never "level approved".

10. The remainder — 21 tier-1 sites, still enumerated

fileswhy not here
plugin-authauth-manager.ts ×9, auth-plugin.ts ×2Judgement-heavy: nine explicit "best-effort — never break the auth response" rationales, each needing its own argument.
plugin-authadmin-user-endpoints.ts, admin-import-users.tsRouted to #13399isMissingTableError lives in a package plugin-auth does not depend on.
plugin-authensure-default-organization.ts ×1⚠️ Still tier-1 DARK. Batch 1's repair there was a LEVEL escalation (warnerror) on a different, quiet site, which correctly does not move a DARK count. Worth a batch of its own.
runtimeaction-execution.ts, domains/keys.tsUnfenced since 77b91bdb4; left for batch 3.
service-storagestorage-service-plugin.ts ×2Composition branches mixed with a probe cleanup; wants a per-site read.
metadata-protocolprotocol.tsReal, in a 17k-line file in a high-traffic package. Its own card.
plugin-sharingshare-link-service.tsJudged OUT (telemetry stamp) — and now the census's dark control, per §6.
verifyharness.tsTest harness.

Authoring session (durable copy — the footer below is rewritten to its bare form by every body edit): https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi


Generated by Claude Code

…h {}` sites swallowed
Batch 2 of #12981's ruled repair-first worklist. The census instrument landed
by batch 1 named seven tier-1 DARK sites in this package; all seven are
repaired through the in-package refusal accumulator, and a census re-run moves
tier 1 from 28 sites in 14 files to 21 in 11, with `channelled` 19 -> 26.
- bootstrap-system-capabilities.ts x2 -- refused insert on the derived half and
refused update on both halves were silent, under an `info` line claiming the
capabilities were seeded. Reported on the durability channel.
- cleanup-package-permissions.ts x1 -- a refused revocation left the grant live
while the package door answered success (ADR-0090 D5 "no ghost grants").
- suggested-audience-bindings.ts x4 -- create/confirm/prune/reap. The insert
site filed every failure under its "benign unique-index race" rationale; the
accumulator's shipped classifier keeps the real race benign and reports the
rest.
Two of the three files keep `warn`: their sinks ride on types exported from
index.ts declaring `warn` optional, so adding `error?` would need a
published-shape break (check:optional-error-sink-contract). The silence is
fixed here; the level is recorded on #12981.
No entry added to scripts/durability-degradation.baseline.json; the gate
vocabulary is untouched in either direction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi
…uled OUT of the programme
Repairing #12981's seven plugin-security sites turned the census instrument's
own `--self-test` red: its `dark` positive control named
`bootstrap-system-capabilities.ts`, with a `why` reading "the card's shape,
verbatim, still standing" -- a sentence batch 2 made false on purpose.
Any tier-1 DARK member of the worklist is a control the repair programme is
designed to destroy, so repointing at another worklist member only moves the
breakage to the batch that repairs that one. The control now names
plugin-sharing's `share-link-service.ts`, which batch 1 judged OUT on the
merits (a use_count/last_used_at telemetry stamp -- escalating a functional
degradation to `error` is the over-application AGENTS.md forbids), so it is a
genuine dark member with a recorded reason to stay one.
Control target only: no predicate, tier or vocabulary change. The instrument is
wired into no workflow (0 references in .github/ and root package.json).
Declared on #12981 before editing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 30, 2026
@github-actions

github-actionsBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx(via sys_permission_set (literal))
  • content/docs/permissions/authorization.mdx(via sys_capability (literal), sys_permission_set (literal))
  • content/docs/permissions/capabilities.mdx(via sys_capability (literal))
  • content/docs/permissions/delegated-administration.mdx(via sys_permission_set (literal))
  • content/docs/permissions/permission-sets.mdx(via sys_capability (literal), sys_permission_set (literal))

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

  • content/docs/releases/v12.mdx(via sys_permission_set (literal))
  • content/docs/releases/v13.mdx(via sys_permission_set (literal))
  • content/docs/releases/v15.mdx(via sys_capability (literal), sys_permission_set (literal))
  • content/docs/releases/v17.mdx(via sys_capability (literal), sys_permission_set (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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 14 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 4cda78c9b3e0de6b99996108e769f434f77258c9packageMentionDocs.

Which tree this was computed on

This run read content/docs from eaae6e4f757bff23f9f5bf695c6c47c5f286def6 — the merge of head b67d7388066a8810e45f0ecab11d02212c2718f4 into base 4cda78c9b3e0de6b99996108e769f434f77258c9, 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 eaae6e4f757bff23f9f5bf695c6c47c5f286def6 && git checkout eaae6e4f757bff23f9f5bf695c6c47c5f286def6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4cda78c9b3e0de6b99996108e769f434f77258c9 b67d7388066a8810e45f0ecab11d02212c2718f4 && git checkout -B drift-repro 4cda78c9b3e0de6b99996108e769f434f77258c9 && git merge --no-ff b67d7388066a8810e45f0ecab11d02212c2718f4
node scripts/docs-audit/affected-docs.mjs --json 4cda78c9b3e0de6b99996108e769f434f77258c9

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

…shift rotted
`check-system-context-census` went red on PR #13414: the elevation read
`if (callerCtx?.isSystem) return;` in `assertTenantAdmin` moved from
suggested-audience-bindings.ts:548 to :703 when this batch inserted the refusal
reporter and its doc above it, and row 58 of
content/docs/permissions/system-context.mdx still cited the old line.
PURE LINE ROT, established before fixing rather than assumed: the census
reports exactly ONE site in this file and the page carries exactly ONE anchor
for it, and the `isSystem` occurrence list is byte-identical to origin/main's
(same 11 occurrences, same text, same order) -- no elevation read was added or
removed. So this is the case the gate repairs mechanically:
`check-system-context-census.mjs --fix` rewrote the one anchor, 548 -> 703.
⛔ Nothing else changed: the row's description, its Get/Lose column and every
other anchor on the page are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi
@os-elon
os-elon added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 71627f7Aug 30, 2026
34 checks passed
@os-elon
os-elon deleted the claude/issue-12981-swallow-batch-2-plugin-security branch August 30, 2026 13:13
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-elon@claude