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.
wpawLognow callsconsole.*directly. - PHP syntax validation passes.
assets/js/sidebar.jsandassets/js/settings-v2.jspass 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 forpostId, but only validates that the id is positive. It does not callcheck_post_permission()before clearing context atincludes/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_memorywithout an upfront post permission check atincludes/class-gutenberg-sidebar.php:2014-2134.handle_block_refine()reads_wpaw_planand tracks cost againstpostIdwithout checking post permissions atincludes/class-gutenberg-sidebar.php:4185-4313.handle_refine_from_chat()passespostIdinto a streaming refinement path without checking post permissions atincludes/class-gutenberg-sidebar.php:4830-4849.handle_seo_audit()reads post content and post config without checking post permissions atincludes/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 atincludes/class-gutenberg-sidebar.php:6032-6057.handle_suggest_keywords()reads detected language and post config forpostIdwithout checking post permissions atincludes/class-gutenberg-sidebar.php:6155-6179.handle_suggest_improvements()reads and parses post content and post config without checking permissions atincludes/class-gutenberg-sidebar.php:6393-6451.
Impact:
- Authors/editors with generic
edit_postscan 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 atincludes/class-gutenberg-sidebar.php:1264-1299.migrate_legacy_chat_history()still keeps_wpaw_chat_historyafter migration because deletion is commented out atincludes/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 atincludes/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_historyafter successful migration, or - Write a durable
_wpaw_chat_history_migratedmarker and never re-import it.
- Delete
- Add migrate-on-read for post-linked contexts.
- Remove or hard-disable
/chat-historyonce 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:
providerselected_providerfallback_usedwarningsmodelcost
- 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 explicitprovider='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.phpandincludes/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_modelas a migration alias into canonicalwriting_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.PluginSidebaris 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_postbefore 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_historymigration 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.