Skip to content

feat(admin): registry moderation UI and blocked-install gating#1977

Merged
ascorbic merged 3 commits into
feat/plugin-registry-labelling-servicefrom
feat/labeler-admin-moderation
Jul 12, 2026
Merged

feat(admin): registry moderation UI and blocked-install gating#1977
ascorbic merged 3 commits into
feat/plugin-registry-labelling-servicefrom
feat/labeler-admin-moderation

Conversation

@ascorbic

@ascorbic ascorbic commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

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.

  • Detail page: each release is evaluated client-side (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 legacy security:yanked (colon) string filter that silently hid yanked releases is removed — a security-yanked (hyphen) release is now visible-but-blocked.
  • Browse cards: mark packages carrying a package- or publisher-scope block, evaluated with a package-only stub mirroring the CLI.
  • Plugin manager (W5.6): consumes the additive moderation field 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 off eligibility (core returns blocked for every clean plugin via missing-assessment-pass) — only blockingLabels/warningLabels.
  • Per-response policy: each aggregator response's atproto-content-labelers header travels with its data via a per-call DiscoveryClient with 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 configured acceptLabelers. Package-scope and release-scope labels arrive on separate responses and are evaluated against the union of both responses' effective policies.
  • Localized labels: a Lingui descriptor map over the 23 canonical label values, falling back to the raw value for anything unknown (never throws, never hides).
  • Structured errors: RELEASE_BLOCKED/RELEASE_YANKED install/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

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation — every new string via Lingui t/Trans/msg; no messages.po changes (catalog extraction runs on merge to main).
  • I have added a changeset
  • New features link to an approved Discussion: RFC RFC: Decentralized Plugin Registry #694

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Code with Claude Fable 5 (design, review, fixes) and Sonnet/Opus subagents (implementation, adversarial review)

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)
  • Typecheck clean; pnpm lint:quick 0 diagnostics; type-aware lint clean on touched files

Try 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.

ascorbic added 2 commits July 12, 2026 12:39
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-bot

changeset-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 09104bb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/admin size/XL labels Jul 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This 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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do 09104bb Jul 12 2026, 12:09 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 09104bb Jul 12 2026, 12:09 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 09104bb Jul 12 2026, 12:09 PM

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:1078

    formatHoldback returns 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) via plural with # minute, # hour, # day.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 12, 2026
Bot review: formatHoldback returned bare English units. Route them
through Lingui plurals like the rest of the admin.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 12, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Fixed in 09104bb. formatHoldback now routes its duration units through Lingui plurals (full words — minute/hour/day — since the abbreviated forms don't translate cleanly).

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.

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 12, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Jul 12, 2026
@ascorbic ascorbic merged commit 02d72af into feat/plugin-registry-labelling-service Jul 12, 2026
10 checks passed
@ascorbic ascorbic deleted the feat/labeler-admin-moderation branch July 12, 2026 12:23
@ascorbic ascorbic mentioned this pull request Jul 12, 2026
81 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin bot:review Trigger an emdashbot code review on this PR cla: signed review/approved Approved; no new commits since size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant