10 KiB
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 checksedit_postbefore clearing post context.- Legacy chat migration now deletes
_wpaw_chat_historyand 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.jsandassets/js/settings-v2.jsstill 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()callsresolve_post_config_from_request()and reads_wpaw_detected_languagebefore checkingedit_postatincludes/class-gutenberg-sidebar.php:2023-2057.handle_block_refine()callsresolve_post_config_from_request()before checkingedit_postatincludes/class-gutenberg-sidebar.php:4203-4230.handle_refine_from_chat()callsresolve_post_config_from_request()before checkingedit_postatincludes/class-gutenberg-sidebar.php:4857-4885.handle_generate_meta()reads post content/title withget_post()before checkingedit_postatincludes/class-gutenberg-sidebar.php:6077-6108.handle_check_clarity()callsresolve_post_config_from_request()before checkingedit_postatincludes/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, requireedit_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()acceptspostIdand records cost against it without checkingedit_postatincludes/class-gutenberg-sidebar.php:6258-6330.handle_detect_intent()acceptspostIdand records cost against it without checkingedit_postatincludes/class-gutenberg-sidebar.php:6357-6415.handle_refine_multi_pass()acceptspostIdand records cost against it without checkingedit_postatincludes/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, requireedit_postbefore using it for cost tracking. - If the endpoint does not need post authority, ignore client-provided
postIdand track cost against0or 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 returnprovider,selected_provider,fallback_used,warnings,model, andcost. - 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_Resultplus 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 atincludes/class-context-service.php:71-73.migrate_legacy_chat_history()signature ismigrate_legacy_chat_history( $post_id )atincludes/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_idwas 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/newsession_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<post_id>\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 torecord_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_modelas a migration alias into canonicalwriting_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
postIdin 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.