# WP Agentic Writer Fifth Retrace Audit Status: COMPLETE / RETRACED Completion marker: 2026-05-25 Follow-up report: `docs/architecture/PLUGIN_AUDIT_RETRACE_SIXTH_PASS_2026-05-25.md` Audit date: 2026-05-25 Baseline retraced: `docs/architecture/PLUGIN_AUDIT_RETRACE_FOURTH_PASS_2026-05-25.md` Scope: fifth pass after fourth-retrace implementation, covering REST authorization, conversation context/history, provider metadata, cost attribution, model defaults, and release readiness. ## Executive Summary The fourth-pass implementation closed meaningful gaps: - `handle_clear_context()` now checks `edit_post` before clearing post context. - Legacy chat migration now deletes `_wpaw_chat_history` and writes `_wpaw_chat_history_migrated`. - `get_context()` now attempts migrate-on-read when no session exists and legacy post meta is present. - `record_usage()` is now marked deprecated. - PHP syntax validation still passes. - `assets/js/sidebar.js` and `assets/js/settings-v2.js` still pass JavaScript syntax validation. The plugin is closer, but not fully clean. The remaining problem is no longer broad absence of checks; it is **ordering and coverage**. Several handlers added permission checks, but some still read post config/meta before checking access. Other AI utility routes accept `postId` for cost attribution or context but still never verify that the current user can edit that post. ## 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 fourth-pass findings against current code. - No live WordPress browser workflow was run in this pass. ## Fourth-Pass Status Trace | Fourth-pass item | Current status | Evidence | |---|---:|---| | Clear-context post auth | Fixed | `handle_clear_context()` checks `check_post_permission()` before clearing at `includes/class-gutenberg-sidebar.php:1240-1263`. | | Legacy chat migration cleanup | Fixed | `migrate_legacy_chat_history()` deletes legacy meta and writes `_wpaw_chat_history_migrated` at `includes/class-context-service.php:310-312`. | | Migrate-on-read behavior | Improved | `get_context()` triggers migration when no session exists and legacy history is present at `includes/class-context-service.php:62-78`. | | Deprecated legacy cost method | Improved | `record_usage()` is marked deprecated at `includes/class-cost-tracker.php:162-188`. | | Permission sweep | Partially fixed | Several routes now check, but some checks still happen after post reads and some post-id routes still lack checks. | | Provider metadata response contract | Still open | Chat exposes provider metadata; non-chat generated responses remain inconsistent. | | Model registry/default unification | Still open | Defaults remain spread across PHP, JS, providers, wrapper, and image manager. | ## Critical Findings ### P0: Some Permission Checks Still Happen After Post-Scoped Reads The fourth-pass report asked for checks before any post read/write/cost attribution/streaming. Some handlers added checks, but they are still too late: - `handle_revise_plan()` calls `resolve_post_config_from_request()` and reads `_wpaw_detected_language` before checking `edit_post` at `includes/class-gutenberg-sidebar.php:2023-2057`. - `handle_block_refine()` calls `resolve_post_config_from_request()` before checking `edit_post` at `includes/class-gutenberg-sidebar.php:4203-4230`. - `handle_refine_from_chat()` calls `resolve_post_config_from_request()` before checking `edit_post` at `includes/class-gutenberg-sidebar.php:4857-4885`. - `handle_generate_meta()` reads post content/title with `get_post()` before checking `edit_post` at `includes/class-gutenberg-sidebar.php:6077-6108`. - `handle_check_clarity()` calls `resolve_post_config_from_request()` before checking `edit_post` at `includes/class-gutenberg-sidebar.php:3809-3839`. Impact: - A user can still trigger reads of post config, detected language, post title/content, or other post-scoped metadata for posts they cannot edit. - These are not merely theoretical because helper calls like `resolve_post_config_from_request()` fall back to stored post config. Recommended fix: - Move the post permission check immediately after extracting `postId`, before any helper call that may read post meta/content. - Use one centralized helper so the ordering is hard to get wrong: - Extract post id. - If `postId > 0`, require `edit_post`. - Only then read config/meta/content or start streaming. ### P0: Some Post-ID Utility Routes Still Have No Target-Post Check Several routes still accept `postId` and use it for context or cost attribution without validating target-post access: - `handle_summarize_context()` accepts `postId` and records cost against it without checking `edit_post` at `includes/class-gutenberg-sidebar.php:6258-6330`. - `handle_detect_intent()` accepts `postId` and records cost against it without checking `edit_post` at `includes/class-gutenberg-sidebar.php:6357-6415`. - `handle_refine_multi_pass()` accepts `postId` and records cost against it without checking `edit_post` at `includes/class-gutenberg-sidebar.php:6730-6782`. Impact: - Cost records can still be attributed to posts the user cannot edit. - These endpoints can be used to pollute another post's cost ledger even if they do not read that post's content directly. Recommended fix: - If an endpoint accepts `postId > 0`, require `edit_post` before using it for cost tracking. - If the endpoint does not need post authority, ignore client-provided `postId` and track cost against `0` or the active session instead. ## High Priority Findings ### P1: Provider Metadata Is Still Not Uniform Outside Chat Provider selection metadata exists in many code paths, but only chat responses consistently expose it. Non-chat flows still often return content, blocks, variants, keyword suggestions, or SEO results without a consistent provider envelope. Examples from the current trace: - Plan/revise/execute/refine handlers use `WP_Agentic_Writer_Provider_Manager::get_provider_for_task()`, but response envelopes do not consistently return `provider`, `selected_provider`, `fallback_used`, `warnings`, `model`, and `cost`. - Keyword and image helper classes unwrap provider results but do not expose fallback metadata to the calling UI consistently. Impact: - Users and support logs still cannot reliably answer "which provider/model served this request?" outside chat. - Fallback behavior is harder to debug in plan, refinement, SEO/GEO, keyword, and image paths. Recommended fix: - Add a shared helper that builds provider metadata from `WPAW_Provider_Selection_Result` plus model/cost response data. - Add it to every generated response and streaming completion event. ### P1: Migrate-On-Read May Not Return the Newly Created Session `get_context()` now calls `migrate_legacy_chat_history( $post_id, $session_id )`, but `migrate_legacy_chat_history()` only accepts `$post_id` in its signature and creates a new session when none exists. After that, `get_context()` tries `$manager->get_session( $session_id )` again. Evidence: - `get_context()` calls migration with two arguments at `includes/class-context-service.php:71-73`. - `migrate_legacy_chat_history()` signature is `migrate_legacy_chat_history( $post_id )` at `includes/class-context-service.php:267`. - When no session exists, migration creates a new session but does not return that new session id at `includes/class-context-service.php:301-307`. Impact: - Legacy history can be migrated and deleted, but the same read may still return an empty context if the caller's original `$session_id` was not the newly created session. - The data is safer than before, but UX may still appear as "history disappeared" until a separate session lookup/list refresh. Recommended fix: - Make `migrate_legacy_chat_history()` return the target/new `session_id`, or accept a requested session id and create/update that session. - In `get_context()`, fetch the returned session id after migration. ## Medium Priority Findings ### P2: Legacy `/chat-history` Endpoint Still Exposes Deprecated Storage The route now has permission checks and a deprecated response marker, and legacy writes are disabled. Still, `/chat-history/(?P\d+)` remains active and reads `_wpaw_chat_history`. Impact: - A deprecated route can keep old UI/client assumptions alive. - It can confuse the source-of-truth contract if any caller still consumes it. Recommended fix: - Return an empty deprecated response after the migration window, or remove the route once the sidebar is fully on conversation sessions. ### P2: Legacy `record_usage()` Still Defaults Provider to OpenRouter The method is deprecated, but it still records provider as `openrouter` for any caller that uses it. Recommended fix: - Change the fallback provider to `unknown`, or require callers to migrate to `record_usage_full()` before accepting records from this compatibility path. ### P2: Model Defaults Remain Fragmented Model defaults still live across activation, settings PHP, settings JS, providers, image manager, and WP AI wrapper. This is unchanged from the fourth pass. Recommended fix: - Add a PHP model registry/default resolver. - Localize resolved defaults to JS. - Treat legacy `execution_model` as a migration alias into canonical `writing_model`. ### P2: Sidebar WordPress Compatibility Still Needs Browser Verification The sidebar still imports `PluginSidebar` from `wp.editPost`. The previous fallback was removed. Syntax is green, but compatibility still needs a browser check on the minimum supported WordPress version. ## Definition of Done Gates for This Pass Before considering this pass complete: - Move every post permission check before any post config/meta/content read. - Add target-post checks or ignore `postId` in summarize-context, detect-intent, and refine-multi-pass. - Make legacy migrate-on-read return the migrated session in the same request. - Standardize provider metadata in non-chat generated responses. - Keep PHP and JS syntax checks green. ## Current Decision The fourth-pass implementation is a real improvement and the legacy history storage is much closer to sane. Do not shift fully into new chat/context work yet. First finish the permission-order sweep and the few remaining cost-attribution endpoints; then the remaining issues are mostly consistency and observability rather than core safety.