Skip to content

feat: multisite-aware block-content abilities via optional blog_id#2669

Merged
chubes4 merged 2 commits into
mainfrom
multisite-content
Jun 16, 2026
Merged

feat: multisite-aware block-content abilities via optional blog_id#2669
chubes4 merged 2 commits into
mainfrom
multisite-content

Conversation

@chubes4

@chubes4 chubes4 commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Makes Data Machine's block-content editing stack multisite-aware. The four content abilities — get_post_blocks, edit_post_blocks, replace_post_blocks, insert_content — now accept an optional blog_id and run their post read/write (and the staged preview→apply cycle) in that blog's context via switch_to_blog().

The gap this closes

The content abilities resolved post_id against whatever blog the request landed on. On a single-site install that's always correct. On a multisite network where the chat surface and the post live on different blogs, it silently targeted the wrong post (or no post at all):

  • get_post() / wp_update_post() / current_user_can('edit_post', …) are all current-blog-scoped.
  • PendingActionStore is $wpdb->prefix-scoped (per-blog), so a staged diff lives in the calling blog's table.
  • Both the propose turn and the accept/reject resolve turn land on the calling blog — so even when a diff card is staged, the apply hits the calling blog's post id.

This surfaced building a chat writing-assistant that edits a draft authored on the main site from a chat drawer running on another subsite (extrachill-roadie#48). The diff/accept-reject substrate (agents-api pending actions + chat DiffCards) is already generic; the missing piece was a target-blog dimension on the content layer.

Design — minimal, content-layer-scoped

blog_id is threaded only through the content abilities and their pending-action handlers. The generic primitives are untouched:

  • No change to PendingActionStore, PendingActionHelper, or ResolvePendingActionAbility.
  • blog_id rides inside the ability input and, for staged edits, inside apply_input — which already round-trips through the per-blog store. The store is staged and resolved on the same (calling) blog, so no cross-blog store lookup is needed; the ability's own switch_to_blog() re-targets the post on both preview and apply.
  • ContentActionHandlers::can_resolve_post_edit() switches to the target blog before the edit_post meta-cap check, since that capability is mapped against the post (and its author) on the blog it lives on.

A new shared BlogContext helper centralizes:

  • target resolution (target_from_input) — returns 0 (no switch) on single-site, when blog_id is omitted, or when it equals the current blog;
  • network-site validation (is_valid) — rejects unknown, archived, deleted, and spam sites;
  • safe switch/restore (enter/leave) with a context token so leave() is a no-op when no switch happened (and ignores WP_Error tokens — no stray restore_current_blog());
  • apply_input/context stamping (with_blog_id) that omits a zero/absent blog_id so single-site payloads stay byte-identical.

Backward compatible: omit blog_id (or run on single-site) and behavior is unchanged.

What changed

  • inc/Abilities/Content/BlogContext.php (new) — the shared targeting/validation/switch helper.
  • GetPostBlocksAbility, EditPostBlocksAbility, ReplacePostBlocksAbility, InsertContentAbility — added blog_id to ability input schema + chat-tool params; wrapped the post read/write in BlogContext::enter()/leave(); stamped blog_id into staged apply_input + preview context. Edit/Replace/Insert split their execute() into a thin context-entering wrapper + execute_in_context() so the switch/restore is guaranteed by finally with minimal churn.
  • ContentActionHandlerscan_resolve_post_edit() now switches to the staged blog_id before the edit_post capability check.

Tests

  • tests/blog-context-content-smoke.php (new, 23 assertions, all pass) — covers target resolution (single-site no-op, same-blog no-op, cross-blog target), network-site validation (valid / unknown / archived / zero), enter/leave switch + restore, no-op tokens, WP_Error tokens, with_blog_id stamping/omission, and the stage→resolve replay round-trip (stamp on blog 1 while the resolve turn lands on blog 12, confirm it re-enters blog 1 and restores blog 12).
  • php -l clean on all six touched/added files; phpcs (project ruleset) 0 findings on all of them incl. the new test.

CI note

The pre-existing pending-action-helper-approval-envelope-smoke.php fails identically on clean main (it depends on the agents-api package absent from a bare composer install) — unrelated to this change, confirmed by reproducing on a fresh clone. Matches the known standalone-smoke fixture debt.

Manual acceptance

On a multisite network, from a chat surface on blog B editing a draft #N that lives on blog A:

  1. get_post_blocks with {post_id: N, blog_id: A} returns blog A's blocks (not blog B's post N).
  2. edit_post_blocks with {post_id: N, blog_id: A, edits: […], preview: true} stages a diff; on accept (resolve_pending_action) the edit applies to blog A's post N, and the edit_post gate is evaluated on blog A.
  3. Omitting blog_id (or on single-site) behaves exactly as before.

Follow-up

Unblocks extrachill-roadie#48 (Roadie writing-assistant editing a Studio draft on main) and any multisite DM install that wants cross-site inline content edits. The Roadie integration lands as a separate PR on top of this.

The block-content abilities (get_post_blocks, edit_post_blocks,
replace_post_blocks, insert_content) resolved post_id against whatever
blog the request landed on. On a multisite network where the chat
surface and the post live on different blogs, that silently targeted the
wrong post (or none). Each ability now accepts an optional blog_id and
runs its post read/write inside switch_to_blog() when the target differs
from the current blog.

The dimension is scoped to the content abilities and their pending-action
handlers; the generic PendingActionStore, PendingActionHelper, and
ResolvePendingActionAbility are untouched. blog_id rides inside the
ability input and, for staged edits, inside apply_input (which already
round-trips through the per-blog store), so the same switch_to_blog()
that runs the preview also runs the apply on accept. The can_resolve
edit_post capability check switches to the target blog first, since the
meta-cap is mapped against the post on the blog it lives on.

A shared BlogContext helper centralizes target resolution, network-site
validation (rejecting archived/deleted/spam sites), and switch/restore.
No-op on single-site or when blog_id is omitted.
@homeboy-ci

homeboy-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Homeboy Results — data-machine

Lint

lint — passed

ℹ️ Full options: homeboy docs commands/lint
Deep dive: homeboy lint data-machine --changed-since 1f4ba4f

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-lint-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-lint-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/27631813232

Test

test — passed

  • 1105 passed
  • 4 skipped

ℹ️ Auto-fix lint issues: homeboy refactor data-machine --from lint --write
ℹ️ Collect coverage: homeboy test data-machine --coverage
ℹ️ Save test baseline: homeboy test data-machine --baseline
ℹ️ Pass args to test runner: homeboy test -- [args]
ℹ️ Full options: homeboy docs commands/test
Deep dive: homeboy test data-machine --changed-since 1f4ba4f

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-test-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-test-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/27631813232

Audit

audit — passed

  • audit — 38 finding(s)
  • Total: 38 finding(s)

Deep dive: homeboy audit data-machine --changed-since 1f4ba4f

Artifacts and drill-down
  • CI results artifact: homeboy-ci-results-data-machine-audit-quality-Linux-node24 contains immediate command JSON for this action invocation.
  • Observation artifact: homeboy-observations-data-machine-audit-quality-Linux-node24 contains exported Homeboy run history for deeper queries.
  • Drill-down: download the observation artifact, then run homeboy runs import <dir>, homeboy runs list, and homeboy runs findings <run-id>.
  • Artifacts are attached to the workflow run: https://github.com/Extra-Chill/data-machine/actions/runs/27631813232
Tooling versions
  • Homeboy CLI: homeboy 0.232.5+edf6d01b7bf5+f9b778b8
  • Extension: wordpress from https://github.com/Extra-Chill/homeboy-extensions
  • Extension revision: ea7980fb
  • Action: unknown@unknown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant