feat(admin): registry moderation UI and blocked-install gating#1977
Conversation
The registry browser now evaluates moderation labels client-side: blocked releases stay visible but their install is gated with an explanation panel, warning-labelled releases surface their warnings in the install consent dialog, browse cards mark blocked packages, and the plugin manager flags blocked or warned updates. Each response is evaluated against the accepted-labeler policy that produced it — the atproto-content-labelers header travels with the data via a per-call discovery client. Localized label names come from a descriptor map over the canonical vocabulary, falling back to the raw value for anything unknown. Removes the legacy security:yanked string filter that silently hid yanked releases.
Adversarial review: package-scope labels ride on the package response while release-scope labels ride on the releases response, each with its own atproto-content-labelers header. Evaluating both against only the releases policy could filter out a package/publisher block whose labeler that response did not report. The two header-derived policies are now unioned. Adds coverage for the header-precedence path and a package-scope block surfaced only via the package response header.
🦋 Changeset detectedLatest commit: 09104bb The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Scope checkThis PR changes 1,537 lines across 12 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 09104bb | Jul 12 2026, 12:09 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 09104bb | Jul 12 2026, 12:09 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 09104bb | Jul 12 2026, 12:09 PM |
There was a problem hiding this comment.
This PR is a well-scoped final consumer of the registry moderation arc. The architecture is sound: client-side moderation evaluation uses per-response atproto-content-labelers headers, package/release policies are unioned, and the UI correctly never gates on eligibility while using blockingLabels/warningLabels. The code reuses Kumo, keeps imports .js-extended, avoids physical-direction Tailwind classes, wraps new UI strings in Lingui, and adds 22 focused tests covering the new flows plus the security-yanked regression.
I checked the full diff and the changed source/test files, traced the registry-client moderation/evaluation helpers, and compared the detail page with the existing marketplace flow. I didn’t run the test suite, linter, or build (no shell/tooling available), so I can’t independently verify the reported pnpm test/lint results beyond saying the code looks consistent.
The only blocking-ish issue I found is one user-facing string that bypasses Lingui, which violates the repo’s localization convention. Everything else is either correct or a minor UX nicety.
Findings
-
[needs fixing]
packages/admin/src/components/RegistryPluginDetail.tsx:1078formatHoldbackreturns raw English duration units (0s,min,h,d) instead of going through Lingui. This string reaches the UI in the holdback banner at line 691 (Your site requires releases to be at least ${formatHoldback(...)} old...), so every locale sees English units.AGENTS.md requires every user-facing admin string to be wrapped for translation.
import { i18n } from "@lingui/core"; import { msg, plural } from "@lingui/core/macro"; function formatHoldback(seconds: number): string { if (seconds <= 0) return i18n._(msg`0s`); if (seconds < 60 * 60) { const m = Math.round(seconds / 60); return i18n._(plural(m, { one: "# min", other: "# mins" })); } if (seconds < 24 * 60 * 60) { const h = Math.round(seconds / 60 / 60); return i18n._(plural(h, { one: "# h", other: "# h" })); } const d = Math.round(seconds / 60 / 60 / 24); return i18n._(plural(d, { one: "# d", other: "# d" })); }If the abbreviated units are too terse for translators, consider full words (
minute/hour/day) viapluralwith# minute,# hour,# day.
Bot review: formatHoldback returned bare English units. Route them through Lingui plurals like the rest of the admin.
|
Fixed in 09104bb. For the record: this function predates this PR (introduced in #1011) and isn't touched by the moderation diff, so it's strictly a pre-existing string. I fixed it anyway because it's in a file this PR already substantially edits and localization is a hard project rule — but flagging the provenance in case you'd rather it lived in its own change. |
There was a problem hiding this comment.
This is a sound, focused final consumer for the registry moderation arc. The architecture remains correct: client-side evaluation uses per-response atproto-content-labelers headers, package/release policies are unioned, and the UI keys off blockingLabels/warningLabels instead of the misleading eligibility field. Kumo primitives are reused, imports keep .js extensions, layout classes are logical, user-facing strings are wrapped in Lingui, and the changeset describes the observable effect.
The one blocking-ish finding from my prior review (raw English duration units in formatHoldback) is now fixed: the helper uses Lingui msg/plural with full words (0 seconds, # minute, # hour, # day).
I re-read the full diff and the changed source/test files, traced the registry-client moderation/evaluation helpers, and verified the new semantic-color usage (kumo-danger) against the admin theme tokens. I did not find any remaining needs_fixing issues. I did not run the test suite, linter, or build (no shell available), so I cannot independently verify the reported pnpm test/lint/typecheck results beyond noting the code looks consistent with those claims.
No line-anchored findings remain; this PR is clean from a static review perspective.
02d72af
into
feat/plugin-registry-labelling-service
What does this PR do?
Adds the admin registry moderation UI — plan items W5.5 and W5.6. This is the last consumer in the aggregator→client moderation arc; the server (core's install/update handlers, merged in #1972) remains the enforcement boundary, and this layer is defense-in-depth plus UX.
evaluateReleaseViews) against the accepted-labeler policy that produced its data. A blocked release stays visible and selectable in the version picker but shows an explanation panel and a disabled Install button; a warning-labelled release surfaces its warnings in the install consent dialog; the version options are annotated with their moderation state. The legacysecurity:yanked(colon) string filter that silently hid yanked releases is removed — asecurity-yanked(hyphen) release is now visible-but-blocked.moderationfield core's update-check now returns; a blocked update is disabled with a badge, a warned update keeps its button with a warning badge. Never keys offeligibility(core returnsblockedfor every clean plugin via missing-assessment-pass) — onlyblockingLabels/warningLabels.atproto-content-labelersheader travels with its data via a per-callDiscoveryClientwith a call-local closure, so a response's labels are only ever judged by the policy that produced them. Query keys that fetch labels now include the configuredacceptLabelers. Package-scope and release-scope labels arrive on separate responses and are evaluated against the union of both responses' effective policies.RELEASE_BLOCKED/RELEASE_YANKEDinstall/update responses are parsed into a typed error rendering a localized headline plus localized blocking-label names.Adversarial review (opus) verdict was ship — no bug above LOW. The LOW (package labels evaluated against the releases response's policy) and both flagged test-coverage gaps (header-precedence path, package-scope block via the real pipeline) are fixed in the final commit: the two header-derived policies are now unioned, with tests for both.
Verification note (please read)
RTL is verified statically: every layout class in the diff is logical (
ms/me/ps/pe/start/end), zero physical-direction classes, confirmed by review. A live Arabic browser pass of the blocked/warned states was not run — those states require a real blocked release, which needs the labeler→aggregator label pipeline that isn't deployed yet. That live pass belongs to the staging-launch phase (the umbrella's consumer-shadow phase) when a blocked release can be produced end-to-end; flagging it as an open verification item rather than claiming a test I couldn't run. All new components reuse Kumo primitives and existing banner/badge patterns already RTL-tested in this admin.Targets
feat/plugin-registry-labelling-service. Related to #1909 and RFC #694.Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runt/Trans/msg; nomessages.pochanges (catalog extraction runs on merge tomain).AI-generated code disclosure
Screenshots / test output
pnpm --filter @emdash-cms/admin test: 89 files, 1088 pass (22 new moderation tests across detail, consent dialog, plugin manager, browse, and the registry-moderation lib; the security-yanked-now-visible regression and the eligibility-not-used property both asserted through the real evaluation pipeline)pnpm lint:quick0 diagnostics; type-aware lint clean on touched filesTry this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
feat/labeler-admin-moderation. Updated automatically when the playground redeploys.