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

14 KiB

WP Agentic Writer Sixth Retrace Audit

Status: COMPLETE / RETRACED
Completion marker: 2026-05-25
Follow-up report: docs/architecture/PLUGIN_AUDIT_RETRACE_SEVENTH_PASS_2026-05-25.md

Audit date: 2026-05-25
Baseline retraced: docs/architecture/PLUGIN_AUDIT_RETRACE_FIFTH_PASS_2026-05-25.md
Scope: sixth pass after fifth-retrace implementation, covering REST authorization, UI/UX risk, conversation context/history, provider transparency, cost attribution, model defaults, and release readiness.

Executive Summary

The fifth-pass implementation closed the main P0 defects that were causing the "fix A, lose B" cycle:

  • Permission checks for the previously flagged post-scoped handlers now happen before post config, meta, content, provider, streaming, or cost work.
  • Utility routes that accept postId for cost/context attribution now validate edit_post before using that post id.
  • Legacy chat migration now has stronger migrate-on-read behavior and deletes old _wpaw_chat_history after migration.
  • PHP and JavaScript syntax checks pass.

I did not find a new P0 blocker in this retrace. The remaining gaps are narrower and mostly about consistency contracts: provider metadata is still only complete in chat, migrate-on-read can still return a stale session_id, legacy chat history is still exposed through a deprecated route, model defaults remain fragmented, and browser-level UI compatibility has not been verified.

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 retrace of fifth-pass findings against current code.
  • Static sweep of provider metadata, context migration, cost tracker, model defaults, and legacy chat history surfaces.
  • No live WordPress editor browser workflow was run in this pass.

Fifth-Pass Status Trace

Fifth-pass item Current status Evidence
handle_revise_plan() permission ordering Fixed postId is extracted and checked before post config/meta/provider work in includes/class-gutenberg-sidebar.php:2023-2057.
handle_block_refine() permission ordering Fixed edit_post is checked before post config/provider work in includes/class-gutenberg-sidebar.php:4207-4236.
handle_refine_from_chat() permission ordering Fixed edit_post is checked before post config/streaming work in includes/class-gutenberg-sidebar.php:4863-4893.
handle_generate_meta() permission ordering Fixed edit_post is checked before get_post() in includes/class-gutenberg-sidebar.php:6085-6108.
handle_check_clarity() permission ordering Fixed edit_post is checked before resolving post config in includes/class-gutenberg-sidebar.php:3811-3839.
handle_summarize_context() post-id authorization Fixed The route validates edit_post before provider/cost work in includes/class-gutenberg-sidebar.php:6266-6278.
handle_detect_intent() post-id authorization Fixed The route validates edit_post before provider/cost work in includes/class-gutenberg-sidebar.php:6374-6388.
handle_refine_multi_pass() post-id authorization Fixed The route validates edit_post before provider/cost work in includes/class-gutenberg-sidebar.php:6756-6770.
Migrate-on-read session recovery Improved, still has one response-identity edge case get_context() uses the migrated id for lookup, but returns the original $session_id in the response at includes/class-context-service.php:72-90.
Provider metadata response contract Still open Chat responses include provider transparency; many non-chat responses still omit it.
Legacy /chat-history endpoint Still open The deprecated route remains registered and still returns legacy post meta in includes/class-gutenberg-sidebar.php:346-354 and includes/class-gutenberg-sidebar.php:1282-1308.
Deprecated cost wrapper Still open as low-risk debt record_usage() is deprecated but still hard-codes provider openrouter in includes/class-cost-tracker.php:176-186.
Model registry/default unification Still open Defaults remain spread across activation, settings, providers, JS presets, wrappers, and image manager.
Sidebar browser compatibility Unverified Syntax passes, but no WordPress editor/browser pass was run.

Remaining Findings

P1: Non-Chat AI Responses Still Do Not Expose Provider Transparency

Chat now satisfies the provider transparency contract by returning provider, selected_provider, fallback_used, and warnings in normal and streaming completion paths at includes/class-gutenberg-sidebar.php:1049-1053 and includes/class-gutenberg-sidebar.php:1220-1227.

The same contract is still missing from many non-chat AI endpoints:

  • Plan generation tracks the actual provider but returns only plan, cost, and web_search_results at includes/class-gutenberg-sidebar.php:1992-2013.
  • Plan revision tracks the actual provider but returns only plan and cost at includes/class-gutenberg-sidebar.php:2153-2172.
  • Block refinement returns blocks, blockId, and cost only at includes/class-gutenberg-sidebar.php:4338-4355.
  • Multi-pass refinement returns pass, refined_content, and cost only at includes/class-gutenberg-sidebar.php:6808-6825.

Impact:

  • Users and support logs can see provider/fallback behavior for chat, but not for planning, writing, refinement, clarity, SEO/meta, or utility AI actions.
  • When fallback routing or local/cloud routing differs by task, the UI cannot explain why costs, quality, or latency changed.
  • Cost records may be accurate internally while the user-visible response stays opaque.

Recommended fix:

  • Add one shared response metadata helper, for example build_provider_metadata( $provider_result ).
  • Add the same keys to every non-streaming AI response:
    • provider
    • selected_provider
    • fallback_used
    • warnings
  • Add equivalent completion metadata to every streaming AI action, not only chat.
  • Add focused REST response tests for plan, revise, block refine, refine multi-pass, clarity, and meta generation.

P1: Migrated Context Can Return Messages From One Session But Report Another Session ID

get_context() now does the important migration recovery step: when the requested session does not exist and legacy post history exists, it migrates the legacy history and then fetches the effective migrated session id.

The remaining problem is the response identity:

  • Migration returns $migrated_session_id at includes/class-context-service.php:72.
  • $effective_session_id is used to fetch $session at includes/class-context-service.php:74-75.
  • The returned context still uses the original $session_id at includes/class-context-service.php:89-90.

Impact:

  • A client can receive migrated messages/context but keep using an empty or stale session id.
  • That can fragment follow-up chat into a different session, making "conversation memory disappeared" bugs look random.
  • This is especially risky during migration from legacy _wpaw_chat_history to conversation sessions because it happens only for older posts.

Recommended fix:

  • After successful migration lookup, set $session_id = $effective_session_id before returning context.
  • Prefer returning $session['session_id'] ?? $effective_session_id if the session object carries its canonical id.
  • Add a migration test where get_context( 'missing-session', $post_id ) creates a new session and verifies that the response session_id equals the migrated session id.

P2: Deprecated /chat-history Still Exposes Legacy Post-Meta History

The legacy route is still registered:

  • GET /wp-agentic-writer/v1/chat-history/(?P<post_id>\d+) remains active at includes/class-gutenberg-sidebar.php:346-354.
  • handle_get_chat_history() still returns values from _wpaw_chat_history with a deprecated marker at includes/class-gutenberg-sidebar.php:1282-1308.
  • get_post_chat_history() still reads _wpaw_chat_history directly at includes/class-gutenberg-sidebar.php:1336-1346.

Impact:

  • This is not an immediate permission issue because the route checks edit_post.
  • It is still product debt because there are now two observable history APIs: legacy post meta and conversation sessions.
  • UI or integrations can keep depending on the old endpoint and bypass the new context/session model.

Recommended fix:

  • Replace the response body with a conversation-session-backed compatibility payload, or return a structured 410/deprecation response after one release window.
  • Track any client code still calling /chat-history before removal.
  • Add a regression test proving no active UI path uses this endpoint.

P2: Deprecated Cost Wrapper Still Defaults Provider To OpenRouter

record_usage() is now explicitly deprecated, which is good. It still calls record_usage_full() with provider openrouter at includes/class-cost-tracker.php:176-186.

Impact:

  • Any remaining caller using the legacy wrapper can create misleading provider attribution.
  • This is lower risk than before because newer paths use provider metadata, but it can still pollute reporting.

Recommended fix:

  • Change the default provider to legacy_unknown or unknown.
  • Log a debug warning when this wrapper is called so remaining callers can be eliminated.
  • Search for all direct calls to record_usage() and migrate them to record_usage_full().

P2: Model Defaults Are Still Fragmented Across Runtime Surfaces

Model defaults are still declared in multiple places with conflicting generations and ids:

  • Activation defaults in wp-agentic-writer.php:140-142.
  • Sidebar defaults in includes/class-gutenberg-sidebar.php:276-283.
  • Settings defaults and sanitization fallbacks in includes/class-settings.php and includes/class-settings-v2.php.
  • Provider property defaults in includes/class-openrouter-provider.php:34-69.
  • JavaScript presets in assets/js/settings-v2.js:35-56.
  • Wrapper fallback groups in includes/class-wp-ai-client-wrapper.php:94-100.
  • Image-manager fallbacks in includes/class-image-manager.php:185-249.

Impact:

  • Changing a default model in one layer can silently diverge from another layer.
  • UI presets, provider runtime defaults, activation defaults, and cost estimation can disagree.
  • This is exactly the kind of area where fixing A can regress B.

Recommended fix:

  • Create one PHP model registry for task defaults, labels, capabilities, pricing hints, provider support, and deprecation status.
  • Generate or localize the JS presets from that registry instead of hard-coding them independently.
  • Add a consistency test that checks all task defaults exist in the registry and that activation/settings/provider fallbacks match it.

P2: Sidebar UI Needs A Real WordPress Editor Compatibility Pass

assets/js/sidebar.js and assets/js/settings-v2.js both pass syntax checks, but syntax is not enough for the editor UI.

Impact:

  • WordPress package availability, wp.editPost.PluginSidebar compatibility, REST nonce behavior, streaming UI states, and cost display states can still fail only inside the block editor.
  • The plugin has many user-facing states now: chat, planning, refinement, clarity, model routing, cost display, warnings, and session history. A static pass cannot validate their interaction quality.

Recommended fix:

  • Run a browser pass inside the WordPress editor with the plugin enabled.
  • Cover at minimum:
    • Sidebar opens and persists.
    • Chat session continues after reload.
    • Provider/fallback warnings render where metadata exists.
    • Cost display updates after chat, plan, refine, and meta-generation actions.
    • Unauthorized post access fails cleanly without partial UI mutation.
    • Model settings changes are reflected in generated requests.

System-Level Opportunities

1. Add A Shared Guard For Post-Scoped REST Requests

The fifth-pass fixes were successful, but they were route-by-route. To keep this from regressing, add a helper such as:

private function require_post_permission_from_request( WP_REST_Request $request, $field = 'postId' ) {
    $post_id = absint( $request->get_param( $field ) );
    if ( $post_id > 0 && ! $this->check_post_permission( $post_id ) ) {
        return new WP_Error( 'forbidden', __( 'You do not have permission to access this post.', 'wp-agentic-writer' ), array( 'status' => 403 ) );
    }
    return $post_id;
}

Then use it immediately after request parsing in every route that reads, writes, streams, or attributes cost to a post.

2. Standardize AI Response Envelopes

The plugin needs one response shape for all AI actions:

{
  "data": {},
  "cost": 0,
  "provider": "openrouter",
  "selected_provider": "openrouter",
  "fallback_used": false,
  "warnings": []
}

The exact shape can differ, but the metadata keys should not. This will make UI, debugging, and cost review dramatically simpler.

3. Treat Conversation Session ID As A Canonical Contract

The remaining migration edge case is a sign that session_id should be treated as a canonical response contract. Any endpoint that creates, migrates, clears, or resumes a session should return the active session id and the UI should update local state from that value.

  1. Fix get_context() so migrated reads return the effective session id.
  2. Add provider metadata to every non-chat AI response and stream completion event.
  3. Replace or retire /chat-history compatibility behavior.
  4. Change deprecated cost wrapper provider attribution from openrouter to legacy_unknown.
  5. Start model registry consolidation.
  6. Run the WordPress editor browser pass before calling the audit chain fully closed.

Current Verdict

The fifth-pass implementation is proper for the critical authorization and cost-attribution issues it targeted. I would mark the fifth-pass P0 items complete.

The plugin is not fully audit-clean yet. The next highest-value work is now chat/context correctness plus response transparency: fix the migrated session_id edge case and make provider/cost metadata consistent across every AI action.