Files
wp-agentic-writer/docs/architecture/PLUGIN_AUDIT_RETRACE_FOURTH_PASS_2026-05-25.md

11 KiB

WP Agentic Writer Fourth Retrace Audit

Status: COMPLETE / SUPERSEDED
Completion marker date: 2026-05-25
Next retrace report: docs/architecture/PLUGIN_AUDIT_RETRACE_FIFTH_PASS_2026-05-25.md

Audit date: 2026-05-25
Baseline retraced: docs/architecture/PLUGIN_AUDIT_RETRACE_THIRD_PASS_2026-05-24.md
Scope: fourth pass after third-retrace implementation, covering UI/UX runtime, REST authorization, conversation context/history, cost tracking, provider/model metadata, migrations, and release readiness.

Executive Summary

The third-pass implementation closed several important items:

  • Sidebar logger recursion is fixed. wpawLog now calls console.* directly.
  • PHP syntax validation passes.
  • assets/js/sidebar.js and assets/js/settings-v2.js pass JavaScript syntax validation.
  • Cost table runtime upgrade now checks for a missing table and recreates it.
  • WP AI wrapper cost tracking now calls record_usage_full() with provider/model metadata instead of the legacy incomplete method.
  • Chat, generate-plan, execute-article, reformat-block, and regenerate-block handlers received target-post permission checks.
  • Clear-context now clears all active sessions for a post when the frontend does not send a sessionId.
  • Legacy update_post_chat_history() no longer writes _wpaw_chat_history.

The remaining blockers are now concentrated in authorization and source-of-truth cleanup. The biggest issue is that several REST handlers still accept postId and read or mutate post-specific data before checking edit_post, or without checking it at all. The second issue is that legacy chat-history migration remains manual and keeps migrated meta in place.

Verification Performed

  • PHP syntax check across plugin PHP files: passed.
  • node -c assets/js/sidebar.js: passed.
  • node -c assets/js/settings-v2.js: passed.
  • Static trace of third-pass findings against current code.
  • No live WordPress browser workflow was run in this pass.

Third-Pass Status Trace

Third-pass item Current status Evidence
Sidebar logger recursion Fixed assets/js/sidebar.js:16-20 now calls console.log/error/info/warn.
Core post-scoped generation auth Partially fixed Chat, generate-plan, execute-article, reformat-block, and regenerate-block now check post permissions, but other post-scoped routes still do not.
Clear-context active session clearing Backend fixed, auth still open Context service clears all active sessions for the post when no sessionId is provided at includes/class-context-service.php:324-345; REST handler lacks target-post permission.
Legacy chat-history helper writes Mostly fixed update_post_chat_history() is now a no-op at includes/class-gutenberg-sidebar.php:1313-1317; migration still does not delete legacy meta or run on read.
WP AI wrapper cost metadata Improved Wrapper uses record_usage_full() at includes/class-wp-ai-client-wrapper.php:197-207 and 254-264.
Cost table missing-table self-heal Fixed maybe_upgrade_table() checks SHOW TABLES LIKE and recreates the table at includes/class-cost-tracker.php:71-80.
Provider metadata outside chat Still open Provider metadata is still not a uniform response contract across non-chat endpoints.
Model registry/default unification Still open Defaults remain spread across activation, settings PHP/JS, providers, image manager, and WP AI wrapper.

Critical Findings

P0: Several Post-Scoped REST Handlers Still Lack Target edit_post Authorization

The previous pass improved a subset of handlers, but the larger REST surface remains inconsistent. These handlers still accept or derive a post id and then read, write, or analyze post-scoped data without a target-post permission check, or they check too late:

  • handle_clear_context() deletes post meta and clears sessions for postId, but only validates that the id is positive. It does not call check_post_permission() before clearing context at includes/class-gutenberg-sidebar.php:1240-1254.
  • handle_revise_plan() reads post config, detected language, post memory, and writes _wpaw_plan, _wpaw_detected_language, and _wpaw_memory without an upfront post permission check at includes/class-gutenberg-sidebar.php:2014-2134.
  • handle_block_refine() reads _wpaw_plan and tracks cost against postId without checking post permissions at includes/class-gutenberg-sidebar.php:4185-4313.
  • handle_refine_from_chat() passes postId into a streaming refinement path without checking post permissions at includes/class-gutenberg-sidebar.php:4830-4849.
  • handle_seo_audit() reads post content and post config without checking post permissions at includes/class-gutenberg-sidebar.php:5714-5735.
  • handle_generate_meta() reads post content before checking permissions; the permission check happens only after content/title are copied at includes/class-gutenberg-sidebar.php:6032-6057.
  • handle_suggest_keywords() reads detected language and post config for postId without checking post permissions at includes/class-gutenberg-sidebar.php:6155-6179.
  • handle_suggest_improvements() reads and parses post content and post config without checking permissions at includes/class-gutenberg-sidebar.php:6393-6451.

Impact:

  • Authors/editors with generic edit_posts can still potentially read or mutate plugin data for posts they cannot edit.
  • The clear-context endpoint can delete another post's agent memory and conversation sessions.
  • SEO/GEO/suggestion routes can leak content, focus keywords, detected language, post config, memory, or outlines.
  • Cost tracking can still be attributed to unauthorized posts from some flows.

Recommended fix:

  • Add a single helper such as require_post_permission_from_params( $params, 'postId' ).
  • Call it before any get_post(), get_post_meta(), get_post_config(), get_post_memory_context(), update_post_meta(), cost attribution, or streaming start.
  • For read-only analysis endpoints, still require current_user_can( 'edit_post', $post_id ) because these routes expose draft/private/editorial data.
  • Add negative permission tests for at least clear-context, revise-plan, SEO audit, generate-meta, block-refine, and suggest-improvements.

High Priority Findings

P1: Legacy Chat History Is Deprecated But Still Not Fully Migrated or Retired

Legacy write paths are improved, but the old store still exists:

  • /chat-history/(?P<post_id>\d+) remains registered and returns _wpaw_chat_history, although it now marks the response as deprecated at includes/class-gutenberg-sidebar.php:1264-1299.
  • migrate_legacy_chat_history() still keeps _wpaw_chat_history after migration because deletion is commented out at includes/class-context-service.php:299-300.
  • The context service header says legacy chat history is migrated on read, but get_context() still returns an empty context when no session exists and does not trigger migration at includes/class-context-service.php:62-87.

Impact:

  • The plugin still has two possible history stores.
  • Legacy meta can be re-imported or returned after a session is cleared or migrated.
  • Chat/context work remains more fragile than it needs to be.

Recommended fix:

  • Decide the final legacy policy:
    • Delete _wpaw_chat_history after successful migration, or
    • Write a durable _wpaw_chat_history_migrated marker and never re-import it.
  • Add migrate-on-read for post-linked contexts.
  • Remove or hard-disable /chat-history once the sidebar fully uses conversations.

P1: Provider Metadata Is Still Not a Uniform Response Contract

Chat responses include provider metadata, but most non-chat generated responses still do not expose it consistently. The code tracks provider metadata in some cost hooks, but the API response contract remains inconsistent for plan, revise, execute, block refinement, keyword, image, SEO/GEO, and suggestion flows.

Impact:

  • Users cannot reliably tell which provider/model actually served a non-chat request.
  • Fallback debugging remains harder than necessary.
  • Cost records and UI messages can diverge.

Recommended fix:

  • Standardize a generated response envelope:
    • provider
    • selected_provider
    • fallback_used
    • warnings
    • model
    • cost
  • Apply it to all generated text/image endpoints and streaming completion events.

Medium Priority Findings

P2: Legacy record_usage() Still Defaults Provider to OpenRouter

Current internal wrapper calls now use record_usage_full(), so the third-pass attribution bug is mostly closed. The older record_usage() compatibility method still records provider as openrouter at includes/class-cost-tracker.php:175-186.

Impact:

  • Any future or external caller using record_usage() can still create misleading provider rows.

Recommended fix:

  • Mark record_usage() as deprecated in the docblock.
  • Either require a provider argument or route it through record_usage_full() with an explicit provider='unknown'.
  • Prefer converting all internal calls to record_usage_full().

P2: Model Defaults Remain Fragmented

Model defaults are still spread across:

  • Activation defaults in wp-agentic-writer.php.
  • Sidebar localized defaults in includes/class-gutenberg-sidebar.php.
  • Settings PHP defaults in includes/class-settings.php and includes/class-settings-v2.php.
  • JS presets in assets/js/settings-v2.js.
  • Provider defaults in OpenRouter, local backend, Codex, and WP AI wrapper classes.
  • Image defaults in includes/class-image-manager.php.

Impact:

  • A model change can fix one runtime path while leaving another path stale.
  • Settings UI, provider runtime, and cost analytics can disagree about the active model.

Recommended fix:

  • Introduce a PHP model registry/default resolver.
  • Localize resolved defaults to JS instead of duplicating them.
  • Treat legacy execution_model as a migration alias into canonical writing_model.

P2: Sidebar WordPress Compatibility Still Needs Browser Verification

The sidebar imports PluginSidebar from wp.editPost at assets/js/sidebar.js:8-10. This may be fine for the target WordPress version, but the previous fallback was removed.

Recommended fix:

  • Browser-test the sidebar on the minimum supported WordPress version.
  • Restore the fallback if wp.editPost.PluginSidebar is not guaranteed.

Definition of Done Gates for This Pass

Before considering this retrace complete:

  • Every REST handler that accepts or derives a post id checks edit_post before post reads, post writes, streaming, or cost attribution.
  • Clear-context cannot clear sessions or meta for a post the current user cannot edit.
  • Legacy _wpaw_chat_history migration either deletes the legacy meta or writes a durable migration marker.
  • get_context() behavior matches its documented migrate-on-read rule.
  • Provider metadata is returned consistently for non-chat generated responses.
  • PHP and JS syntax checks remain green.

Current Decision

The third-pass implementation is meaningfully better, but the plugin is still not clean enough to shift fully into new chat/context work. Finish the remaining post-scoped authorization sweep first, then close the legacy history migration gap. After that, chat/context implementation will be much less likely to keep reopening old regressions.