# 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\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.