Feature 065 — safety envelope + payload enrichment across 9 existing abilities. No new abilities. Plugin version bumped 0.0.28 → 0.0.29.
Two changes are breaking for programmatic callers: acrossai/delete-media and acrossai/delete-file now require an explicit confirm: true; acrossai/update-post silently strips protected meta keys (reported back in dropped_meta_keys). Every guardrail-triggered refusal returns success: false + a machine-readable blocked_reason + a human message, with no state mutation on the refusal path.
What ships
acrossai/deactivate-plugin— protected-plugin guard. Refuses to deactivateacrossai-mcp-manager,acrossai-abilities-manager, oracrossai-pro— the three plugins that host either the ability surface itself or the MCP transport the AI is using to reach the site. Match runs against the resolved plugin file path, so slug / partial-name / file-path variants that fuzzy-resolve to a protected plugin are all refused (blocked_reason: "protected_plugin").acrossai/delete-media— explicit confirmation + trash-aware. Requiresconfirm: true(refuses withblocked_reason: "confirmation_required"otherwise). Honours theMEDIA_TRASHconstant — trashes when defined truthy andforceis absent; permanent-deletes otherwise. Response now carriesdeleted: "deleted" | "trashed".acrossai/delete-file— confirmation + protected-write + backup + opcache invalidation. Requiresconfirm: true. Refuses onwp-config.php/.htaccessat ABSPATH (blocked_reason: "protected_write"). Writes a.bak.<timestamp>copy next to the target before the delete and returns the backup path inbackup. Callsopcache_invalidate()on the deleted path when OPcache is loaded.acrossai/read-file— protected-read + size cap + binary detection. Refuses onwp-config.php/.htaccessat ABSPATH (blocked_reason: "protected_read") — this closes the highest-value accidental disclosure path (database password + eight auth constants). Refuses files over 5 MB without loading them into memory (blocked_reason: "file_too_large"; response reports observed size + cap). Non-UTF-8 payloads return{ binary: true, size, path, message }instead of raw bytes.acrossai/list-media— alt-text search.searchnow matches against_wp_attachment_image_altpostmeta in addition to WP_Query's defaultsfields (title / caption / description). Results are de-duplicated by attachment ID, so an image matched by both title and alt-text appears once.acrossai/update-media— updated-fields report. Response now carries anupdatedarray naming each field that was actually written (subset oftitle/caption/description/alt_text), in the order fields were processed. Empty array when no update fields were passed.acrossai/update-post— writability + protected-meta + publish / author gates. Refuses on post types that are neitherpublic: truenorshow_in_rest: true(matches WP-REST writability). Filters caller-suppliedmetato drop_-prefixed keys and any key thatis_protected_meta()reports; theacrossai_allowed_protected_metafilter opts specific keys back in. Dropped keys are reported in the response asdropped_meta_keys. Refusesstatus: "publish"(or any status entering a public state) unless the caller holdspublish_postsfor the post type. Refusesauthor: <different_user_id>unless the caller holdsedit_others_posts.acrossai/get-post— hydrated payload. Response now includesterms(object keyed by taxonomy, each entry{ term_id, name, slug }),meta(non-protected keys only — same allow-list filter asupdate-post),featured_image({ id, url, alt }ornull),permalink,edit_link, andauthor: { id, name }. Callers no longer need 4–5 follow-up hydration calls per post.acrossai/delete-post— suggested-redirect hint. When the target waspublishandforce: trueis passed, the response includessuggested_redirect: { from: <permalink>, to: <parent-or-archive-or-root-url> }. Omitted for drafts and for trash operations (URL may return on restore).
Test coverage. Test_Feature_065_Safety_And_Payload — 23 source-inspection tests covering all 23 FRs. Full suite green; PHPCS (WPCS strict) and PHPStan level 8 clean.
PR: #133 (release cut) — shipping code from #114 merged earlier at 1182ec3.