Skip to content

feat(plugins): add publication policy hooks - #3185

Merged
ascorbic merged 17 commits into
mainfrom
codex/plugin-publication-policy
Sep 20, 2026
Merged

ascorbic merged 17 commits into
mainfrom
codex/plugin-publication-policy

Conversation

@ascorbic

@ascorbic ascorbic commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds capability-gated publication policy hooks for native and sandboxed plugins without granting content read, write, or lifecycle authority. Events identify every action origin and authenticated human actor.

Cancellation is validated and revision-fenced. Scheduler rejections become bounded dashboard records with revision-safe cleanup. Visual-editor actions use renewable editor-bound tokens.

Includes registry consent, both runners, runtime-backed tests, docs, authoring guidance, and a changeset. Builds on #3162.

Type of change

  • Bug fix
  • Feature (maintainer-directed follow-on to feat(plugin-test): add runtime-backed host #3162; no separate Discussion URL)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • Targeted behavior tests pass
  • pnpm format has been run
  • Tests are added/updated
  • Admin strings use Lingui; no catalogs included
  • Changeset added and reviewed
  • Approved Discussion link (not applicable; maintainer-directed capability plan following feat(plugin-test): add runtime-backed host #3162)
  • Rendered UI screenshot included

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: gpt-5.6-sol

Screenshots / test output

Admin dashboard showing scheduled entries blocked by publication policy

Validated with root build/typecheck, lint, docs, focused tests, query counts, and adversarial review.

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 2,486 lines across 77 files. Large PRs are harder to review and more likely to be closed without review.
This PR spans 4 different areas (area/core, area/admin, area/docs, area/cloudflare). Consider breaking it into smaller, focused PRs.

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.

@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cc676a8

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

This PR includes changesets to release 18 packages
Name Type
emdash Minor
@emdash-cms/plugin-types Minor
@emdash-cms/plugin-cli Minor
@emdash-cms/plugin-test Minor
@emdash-cms/registry-lexicons Minor
@emdash-cms/admin Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Minor
@emdash-cms/registry-verification Patch
@emdash-cms/registry-client Patch
@emdash-cms/registry-loader Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
create-emdash Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
@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

@ascorbic
ascorbic marked this pull request as ready for review September 17, 2026 18:02
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@github-actions github-actions Bot added cla: signed review/needs-review No maintainer or bot review yet labels Sep 17, 2026
@pkg-pr-new

This comment has been minimized.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://codex-plugin-publication-policy.try.emdashcms.com, https://codex-plugin-publication-policy-emdash-playground.emdash-cms.workers.dev (commit cc676a8)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://fb056090.try.emdashcms.com, https://fb056090-emdash-playground.emdash-cms.workers.dev cc676a8 2026-09-20T01:08:50.692Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://874d3066.try.emdashcms.com, https://874d3066-emdash-playground.emdash-cms.workers.dev 9c9c5a2 2026-09-20T00:09:50.946Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://6635599f.try.emdashcms.com, https://6635599f-emdash-playground.emdash-cms.workers.dev 9bd2738 2026-09-19T23:20:32.923Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://e9affd92.try.emdashcms.com, https://e9affd92-emdash-playground.emdash-cms.workers.dev 86ad0ac 2026-09-19T21:35:22.797Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://43d192a4.try.emdashcms.com, https://43d192a4-emdash-playground.emdash-cms.workers.dev cadfc4f 2026-09-19T20:22:38.593Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://4a64d0bf.try.emdashcms.com, https://4a64d0bf-emdash-playground.emdash-cms.workers.dev 1ef2cfb 2026-09-19T19:56:43.577Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://ec998c5c.try.emdashcms.com, https://ec998c5c-emdash-playground.emdash-cms.workers.dev 4c88061 2026-09-19T19:44:56.890Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://a3262263.try.emdashcms.com, https://a3262263-emdash-playground.emdash-cms.workers.dev 3a61f41 2026-09-19T13:40:10.082Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://f42c980a.try.emdashcms.com, https://f42c980a-emdash-playground.emdash-cms.workers.dev 2dc16ab 2026-09-19T13:05:23.724Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://305a89b7.try.emdashcms.com, https://305a89b7-emdash-playground.emdash-cms.workers.dev fabefe3 2026-09-19T10:59:21.611Z Visit the dashboard ↗

View all previews: View all previews ↗

Comment thread packages/core/tests/unit/visual-editing/toolbar.test.ts Fixed
Comment thread packages/core/tests/unit/visual-editing/toolbar.test.ts Fixed
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond overlap and removed review/needs-review No maintainer or bot review yet labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@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 follow-on to #3162 is the right scoped addition: it gates publication, scheduling, and unpublication through capability-registered policy hooks, persists scheduler rejections as versioned dashboard notices, and protects visual-editor publish actions with renewable tokens, all without granting content read/write authority to policy plugins.

I re-checked the two concrete gaps from the prior review and both are resolved on this head:

  • packages/core/src/mcp/server.ts now requires _rev on the content_schedule tool and forwards it to handleContentSchedule, so MCP scheduling has the same lost-update protection as REST scheduling, with tests in packages/core/tests/integration/mcp/content-misc.test.ts.
  • packages/core/src/visual-editing/toolbar.ts renders all toolbar label strings from ToolbarLabels, and packages/admin/src/locales/server.ts exposes Lingui message descriptors for every label, satisfying the admin-string localization requirement.

Beyond those fixes, I re-reviewed the policy implementation, runtime hooks, REST and MCP route origin/actor plumbing, scheduler rejection persistence and cleanup, dashboard/auth wiring, and the visual-editing token flow. No new logic bugs, security issues, data-loss concerns, AGENTS.md violations, or missing tests were found. The changeset is detailed and the docs match the implemented surface.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@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 Sep 19, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 19, 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.

Re-review of PR #3185 (feat(plugins): add publication policy hooks). The two previously-requested fixes are present and the new policy/runtime surface is sound. No blocking issues remain, but there are AGENTS.md comment-discipline violations in files touched by this PR.

Confirmed fixes:

  • MCP content_schedule now requires _rev: the MCP tool-level input schema enforces it while the REST POST /_emdash/api/content/{collection}/{id}/schedule keeps _rev optional for legacy callers.
  • Admin toolbar labels are localized via Lingui: Shell.tsx writes t rendered strings to localStorage; the public fallback endpoint GET /_emdash/api/visual-editing/toolbar-labels returns the standard { data: { editMode, hideToolbar } } envelope and uses private, no-store caching.

Other convention checks:

  • UI: new/updated admin and toolbar code uses Lingui macros (t, plural) and logical Tailwind utilities (me-auto, text-start, etc.); Dashboard.tsx wraps counts in <bdi dir="ltr"> for RTL safety.
  • SQL: all prefix scans in OptionsRepository use escapeLike(...) and ESSE '\\', and content/policy actions route through repositories with parameterized queries.
  • Authorization: content mutation routes gate actions via requireOwnerPerm/content:publish_any; dismissal of scheduled rejections requires content:publish_any.
  • Query counts: the logged-out cold-start snapshot's single name LIKE ... option query is a replacement, not an addition; hook resolution uses batched IN (...) startup reads.
  • API envelope: the new public endpoint returns { data: ... }, not a naked value.

Remaining AGENTS.md violation: three files changed in this PR still contain prohibited issue/PR references (Discussion #1742) in comments. AGENTS.md explicitly forbids referencing issues, PRs, or review threads in code comments; that context belongs in commits/PR descriptions. I have line-anchored these below.


Findings

  • [needs fixing] packages/admin/src/components/Shell.tsx:75-76

    AGENTS.md comment-discipline violation: this comment references Discussion #1742. Issue/PR references are prohibited in code comments because they become stale narrative immediately after merge; that context belongs in the commit message or PR description. Describe the invariant directly instead.

    	// Maintain the non-secret "an editor session may exist in this browser"
    	// localStorage flag consumed by the public-site toolbar bootstrap
    	// (`toolbar: "client"`). Set here — not in the login
    	// flows — so every auth method (passkey, OAuth, magic link, dev bypass)
    	// is covered. Opening the admin also un-dismisses the toolbar.
    	// Key literals are duplicated in emdash core, which the admin can't import.
    
  • [needs fixing] packages/core/src/visual-editing/toolbar-bootstrap.ts:14

    AGENTS.md comment-discipline violation: See Discussion #1742 is an issue/PR reference and is not allowed in code comments. Replace it with an evergreen explanation of why only the editor flag is checked.

     * nothing else. Only the editor flag gates injection; missing localized
     * labels are fetched lazily rather than blocking the pill from mounting.
    
  • [needs fixing] packages/core/tests/unit/astro/request-context-toolbar-mode.test.ts:2

    AGENTS.md comment-discipline violation: the module comment references Discussion #1742. Remove the issue reference.

    /**
     * Tests for the `toolbar` config modes.
     *
     * - `"server"` (default): current behavior — the toolbar is injected
     *   server-side for authenticated editors.
     * - `"client"`: public HTML is identical for everyone (bootstrap script,
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Sep 19, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 19, 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.

Re-review of PR #3185. The three Discussion #1742 comment-discipline findings from the previous review are fixed in the files the prior review called out (Shell.tsx, toolbar-bootstrap.ts, request-context-toolbar-mode.test.ts).

The publication-policy hook design is sound and fits EmDash's architecture: capability-gated registration via hooks.content-policy:register, revision-fenced mutations, origin/actor-aware events for API/MCP/visual-editor/scheduler/system, scheduler rejection cleanup through the options table, and public-safe dashboard records. The changeset accurately describes the surface area.

Two new issues remain:

  1. The dashboard dismissal endpoint is not wired up. packages/core/src/astro/routes/api/admin/scheduled-policy-rejections/[collection]/[id].ts is implemented and unit-tested, but packages/core/src/astro/integration/routes.ts never calls injectRoute() for it. Admin dashboard dismissal calls will 404, so the "blocked scheduled content" notice cannot be cleared through the UI. This defeats a headline feature of the PR and needs fixing before merge.

  2. Stale issue/PR references in a file changed by this PR. packages/core/src/astro/middleware/request-context.ts still contains three prohibited references (Discussion #1742 × 2 and #1398 × 1) in comments. AGENTS.md forbids these; the context belongs in commit messages/PR descriptions, not code comments.

Beyond these, the runtime wiring, MCP integration, visual-editing action-token renewal, SQL safety on prefix scans, authorization gating, and Lingui/RTL-safe admin UI look correct.


Findings

  • [needs fixing] packages/core/src/astro/integration/routes.ts:119-122

    The new DELETE /_emdash/api/admin/scheduled-policy-rejections/:collection/:id handler exists and is tested, but it is never registered here. The admin dashboard calls this path to dismiss blocked scheduled-content notices; without injectRoute() the endpoint returns 404 and the dismissal feature is broken.

    Add the route next to the related dashboard route.

    	injectRoute({
    		pattern: "/_emdash/api/dashboard",
    		entrypoint: resolveRoute("api/dashboard.ts"),
    	});
    
    	injectRoute({
    		pattern: "/_emdash/api/admin/scheduled-policy-rejections/[collection]/[id]",
    		entrypoint: resolveRoute("api/admin/scheduled-policy-rejections/[collection]/[id].ts"),
    	});
    
  • [needs fixing] packages/core/src/astro/middleware/request-context.ts:13

    AGENTS.md comment-discipline violation: this comment references Discussion #1742. Issue/PR references are prohibited in code comments because they become stale narrative immediately after merge; that context belongs in the commit message or PR description. Describe the invariant directly instead.

     *   renders; the backend verifies the `_edit` param and redirects non-editors.
    
  • [needs fixing] packages/core/src/astro/middleware/request-context.ts:108

    AGENTS.md comment-discipline violation: (#1398) is an issue/PR reference. Replace it with an evergreen explanation of why both cache layers must be disabled.

    		// Both layers must be disabled: `Cache-Control` covers browsers/downstream
    		// proxies; the route-cache opt-out covers the shared edge cache, which ignores
    		// `Cache-Control`.
    
  • [needs fixing] packages/core/src/astro/middleware/request-context.ts:130

    AGENTS.md comment-discipline violation: this comment references Discussion #1742. Remove the issue reference and state the behavior directly.

     * `?_edit` link degrades gracefully for everyone else: non-editors are redirected
     * to the canonical URL.
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@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 Sep 19, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 19, 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.

Re-review of PR #3185. The publication-policy hook design remains sound: hooks.content-policy:register is capability-gated, native and sandboxed runners receive the new hooks, origin/actor metadata flows through the runtime for API/MCP/visual-editor/scheduler/system actions, scheduler rejections are persisted as bounded dashboard records with revision-fenced cleanup, and the admin UI correctly localizes the dismissal flow.

However, the two concrete findings from the previous review are still not fixed in this revision.

  1. Dashboard dismissal route is still not wired up. packages/core/src/astro/routes/api/admin/scheduled-policy-rejections/[collection]/[id].ts exists and has unit tests, but packages/core/src/astro/integration/routes.ts never calls injectRoute() for it. The admin dashboard issues DELETE /_emdash/api/admin/scheduled-policy-rejections/:collection/:id to dismiss blocked scheduled-content notices, so those calls will 404 and the dismissal feature will not work.

  2. Stale issue/PR references remain in request-context.ts. Three comments still reference Discussion #1742 and #1398, which violates AGENTS.md comment discipline: issue/PR references are stale narrative after merge; that context belongs in commit messages and PR descriptions.

Everything else checked — route authorization on the dismissal handler, dashboard stats SQL safety, visual-editing action-token/toolbar-labels wiring, Lingui server-label loading, RTL-safe admin markup, and the scheduler rejection path — looks correct. Please add the missing route and remove the three issue references, then this should be good to merge.


Findings

  • [needs fixing] packages/core/src/astro/integration/routes.ts:119-122

    The new DELETE /_emdash/api/admin/scheduled-policy-rejections/:collection/:id handler exists and is unit-tested, but it is never registered here. The admin dashboard calls this path to dismiss blocked scheduled-content notices; without injectRoute() the endpoint returns 404 and the dismissal feature is broken.

    Add the route next to the related dashboard route.

    	injectRoute({
    		pattern: "/_emdash/api/dashboard",
    		entrypoint: resolveRoute("api/dashboard.ts"),
    	});
    
    	injectRoute({
    		pattern: "/_emdash/api/admin/scheduled-policy-rejections/[collection]/[id]",
    		entrypoint: resolveRoute("api/admin/scheduled-policy-rejections/[collection]/[id].ts"),
    	});
    
  • [needs fixing] packages/core/src/astro/middleware/request-context.ts:13

    AGENTS.md comment-discipline violation: this comment references Discussion #1742. Issue/PR references are prohibited in code comments because they become stale narrative immediately after merge; that context belongs in the commit message or PR description. Describe the invariant directly instead.

     *   renders; the backend verifies the `_edit` param and redirects non-editors.
    
  • [needs fixing] packages/core/src/astro/middleware/request-context.ts:108

    AGENTS.md comment-discipline violation: (#1398) is an issue/PR reference. Replace it with an evergreen explanation of why both cache layers must be disabled.

    		// Both layers must be disabled: `Cache-Control` covers browsers/downstream
    		// proxies; the route-cache opt-out covers the shared edge cache, which ignores
    		// `Cache-Control`.
    
  • [needs fixing] packages/core/src/astro/middleware/request-context.ts:130

    AGENTS.md comment-discipline violation: this comment references Discussion #1742. Remove the issue reference and state the behavior directly.

     * `?_edit` link degrades gracefully for everyone else: non-editors are redirected
     * to the canonical URL.
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Sep 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 20, 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.

Re-review of PR #3185. The publication-policy hook design remains the right change for the stated goal: hooks.content-policy:register is capability-gated, native and sandboxed runners enforce it, origin/actor metadata flows through API/MCP/visual-editor/scheduler paths, scheduler rejections are persisted as bounded dashboard records with revision-fenced cleanup, and the admin UI localizes and authorizes the dismissal flow.

Prior findings are fixed. The DELETE /_emdash/api/admin/scheduled-policy-rejections/:collection/:id route is now registered in packages/core/src/astro/integration/routes.ts, and the stale Discussion #1742 / #1398 issue references have been removed from request-context.ts.

What I checked: policy decision validation, hook registration/capability gating, runtime policy evaluation and revision fencing, scheduler rejection persistence and cleanup, dashboard handler SQL safety and authorization, visual-editing action-token renewal, route wiring, Lingui localization and RTL-safe admin markup, query-count snapshots, changeset prose, and documentation accuracy.

Headline conclusion: One remaining issue: the new public /_emdash/api/visual-editing/toolbar-labels route returns a raw { data: … } shape instead of the standard { success: true, data: … } API envelope. The client bootstrap still works because it reads body.data, but the endpoint is inconsistent with every other route and locks the shape in via its test. Convert it to apiSuccess() and update the route test.

Comment thread packages/core/src/astro/routes/api/visual-editing/toolbar-labels.ts Outdated
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 20, 2026
@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 Sep 20, 2026
@github-actions github-actions Bot added the review/needs-rereview Author pushed changes since the last review label Sep 20, 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.

I reviewed the full diff for PR #3185 (publication-policy hooks, dashboard scheduler warnings, and visual-editing toolbar labels) plus the key changed files.

  • Architecture & auth: Policy hooks run through the existing capability-gated hook pipeline; the new hooks.content-policy:register capability is documented. REST/MCP/visual-editor routes pass origin/actor so policy events can identify the source. The dismissal route checks content:publish_any and uses compare-and-delete on the stored rejection revision.
  • Scheduler cursor: publishDueContent uses a per-collection compare-and-set cursor, advances (even on publish failures) so the sweep drains the backlog, and compare-and-set with a stale revision prevents older concurrent sweeps from moving the cursor backwards.
  • Dashboard: handleDashboardStats safely queries by prefix with escaped LIKE, and the UI wires dismissal with query invalidation and conflict handling.
  • Visual-editing labels: GET /_emdash/api/visual-editing/toolbar-labels now returns the standard { success, data } apiSuccess envelope, is listed as a public API route, and is used only for the client-mode bootstrap. Toolbar injection continues to set Cache-Control: private, no-store and opt out of the shared route cache.
  • Changeset: The publication-policy-hooks.md changeset accurately describes the new capability, hook events, reason validation, rejection record lifecycle, and dashboard dismissal.
  • AGENTS.md conventions: No stale issue/PR references in new comments, no SQL interpolation, authorization uses RBAC permissions from rbac.ts, and user-facing admin strings go through Lingui.

No blocking issues, regressions, or convention violations were found. The PR is clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants