---
name: ms-chat WordPress Agent Tools (WP plugin)
overview: RANK AI plugin registration, connect/disconnect UI, agent REST API, and deep-link confirm flow for ms-chat integration.
project: rank-ai-by-morningscore
related: msio (jetforce-wp + ms-chat)
todos:
  - id: phase-a-registration
    content: "Registration service: register/unregister on key save, activation, deactivation, daily cron"
    status: pending
  - id: phase-b-connect-ui
    content: "Connect/disconnect UI on API key tab, signed state handler, deep-link redirect to returnUrl"
    status: pending
  - id: phase-c-agent-rest
    content: "Agent REST routes + auth middleware (HMAC + service secret)"
    status: pending
  - id: phase-d-services-refactor
    content: "Extract shared services from AJAX handlers for agent + admin reuse"
    status: pending
  - id: phase-e-agent-endpoints
    content: "Job status + mutation endpoints (health, booster, writer)"
    status: pending
isProject: false
---

# ms-chat WordPress Agent Tools — WP Plugin Implementation Plan

Companion doc for msio services: [`ms-chat-msio-implementation.md`](./ms-chat-msio-implementation.md)

## Overview

This plugin gains three capabilities:

| Capability | Purpose |
|------------|---------|
| **Registration** | Tell jetforce-wp this site exists + plugin version (daily + on key save) |
| **Connect flow** | One-click link ms-chat agent (proves WP admin ownership) |
| **Agent REST** | Authenticated endpoints ms-chat calls for writes + job status |

**Not in this plugin:** health issue counts, domain list, articles list — ms-chat reads those from api.morningscore.io directly.

One confirm per WP install — not per WPML language.

---

## Architecture (plugin role)

```
  ms-chat "Connect in WordPress →"
       |
       v
  /wp-admin/admin.php?page=rank-admin&tab=api_config&ms_chat_state={signed}
       |
       | User clicks [Connect ms-chat] (manage_options required)
       v
  Plugin:
    1. Verify ms_chat_state signature (if present)
    2. Generate rank_ms_chat_agent_token (stored locally only)
    3. POST jetforce-wp /api/auth/ms-chat/confirm
    4. If deep link: redirect → returnUrl?linked=true
       |
       v
  ms-chat calls WP REST for writes:
    /wp-json/rank-ai/v1/agent/*
    Headers: X-MS-Agent-Secret, X-MS-Site-Token, X-MS-Signature
```

---

## Registration & heartbeat

### When to register

| Trigger | Hook / action |
|---------|---------------|
| API key saved | `update_option_rank_api_key` (priority 20) |
| Plugin activated | `rank_activate_plugin` |
| Plugin deactivated | `rank_deactivate_plugin` → unregister |
| Daily cron | `rank_ms_chat_daily_register` |

### Register function

```php
function rank_ms_chat_register(): void {
    $api_key = rank_get_api_key();
    if ( empty( $api_key ) ) {
        return;
    }

    $payload = array(
        'site_url'        => home_url(),
        'plugin_version'  => rank_get_plugin_version(),
        'wp_version'      => get_bloginfo( 'version' ),
        'php_version'     => PHP_VERSION,
    );

    $fingerprint = md5( wp_json_encode( $payload ) );
    $last        = get_option( 'rank_ms_chat_last_register', array() );

    // Skip if unchanged and registered within 20h (except forced calls)
    if (
        ! empty( $last['fingerprint'] )
        && $last['fingerprint'] === $fingerprint
        && ( time() - (int) $last['at'] ) < 20 * HOUR_IN_SECONDS
    ) {
        return;
    }

    $response = wp_remote_post(
        rank_ms_chat_registry_url( '/api/wp/register' ),
        array(
            'headers' => array(
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ),
            'body'    => wp_json_encode( $payload ),
            'timeout' => 15,
        )
    );

    if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
        update_option( 'rank_ms_chat_last_register', array(
            'fingerprint' => $fingerprint,
            'at'          => time(),
        ) );
    }
}
```

### Unregister on deactivation

```php
function rank_ms_chat_unregister(): void {
    $api_key = rank_get_api_key();
    if ( empty( $api_key ) ) {
        return;
    }

    wp_remote_post(
        rank_ms_chat_registry_url( '/api/wp/unregister' ),
        array(
            'headers' => array(
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'application/json',
            ),
            'body'    => wp_json_encode( array( 'site_url' => home_url() ) ),
            'timeout' => 10,
        )
    );

    delete_option( 'rank_ms_chat_last_register' );
}
```

### Daily cron setup

```php
// rank-main.php or class-rank-ms-chat.php
add_action( 'rank_ms_chat_daily_register', 'rank_ms_chat_register' );

function rank_ms_chat_schedule_cron(): void {
    if ( ! wp_next_scheduled( 'rank_ms_chat_daily_register' ) ) {
        wp_schedule_event( time(), 'daily', 'rank_ms_chat_daily_register' );
    }
}
register_activation_hook( __FILE__, 'rank_ms_chat_schedule_cron' );
register_deactivation_hook( __FILE__, function () {
    wp_clear_scheduled_hook( 'rank_ms_chat_daily_register' );
    rank_ms_chat_unregister();
} );
```

### Privacy note (API key tab)

> Registers this site's URL and plugin version with Morningscore daily so the chat agent knows what's installed. No content or health data is sent.

---

## WP options

| Option | Type | Purpose |
|--------|------|---------|
| `rank_ms_chat_agent_token` | string | 64-char secret; never sent to central in plaintext |
| `rank_ms_chat_resource_id` | string | Linked ms-chat resource ID |
| `rank_ms_chat_linked_at` | int | Unix timestamp of connect |
| `rank_ms_chat_last_register` | array | `{ fingerprint, at }` for dedup |

---

## Connect / disconnect UI

### Location

API key tab (`tab=api_config`) in `admin/admin-page.php` — section below the API key form.

### States

**Not connected:**

```
Morningscore Agent (ms-chat)
─────────────────────────────
Status: Not connected

Allow your Morningscore chat agent to run health fixes,
page booster, and rank writer on this site.

[ Connect ms-chat ]
```

**Connected:**

```
Status: ✓ Connected
Linked: Jul 7, 2026

[ Disconnect ]
```

**Arrived via deep link** (`ms_chat_state` in URL): highlight Connect button + short explanation ("ms-chat is requesting access to this site").

### Connect flow (AJAX)

```
POST wp_ajax_rank_ms_chat_connect
  1. verify_ajax_request (manage_options)
  2. Parse ms_chat_state from POST if present → verify via jetforce-wp or local HMAC
  3. Generate agent_token = wp_generate_password( 64, false )
  4. POST jetforce-wp /api/auth/ms-chat/confirm:
       resource_id, site_url, agent_token_hash, ms_chat_state
  5. Store rank_ms_chat_agent_token, resource_id, linked_at
  6. Return { success: true, redirect_url: returnUrl?linked=true } or { success: true }
```

### Disconnect flow

```
POST wp_ajax_rank_ms_chat_disconnect
  1. verify_ajax_request
  2. POST jetforce-wp /api/auth/ms-chat/revoke
  3. Delete rank_ms_chat_* options
```

### Deep-link redirect (v1)

When connect succeeds and `ms_chat_state` contained `returnUrl`:

```php
// In connect handler response or server-side redirect
$return_url = add_query_arg( 'linked', 'true', $verified_state['return_url'] );
wp_redirect( $return_url );
exit;
```

For AJAX connect initiated from deep-link page, return `redirect_url` to JS:

```js
// admin/js/ms-chat-connect.js
if (response.redirect_url) {
  window.location.href = response.redirect_url;
}
```

Manual connect (no state): stay on page, show success notice.

---

## Signed state handling

### On page load (API key tab)

```php
function rank_ms_chat_parse_state(): ?array {
    if ( empty( $_GET['ms_chat_state'] ) ) {
        return null;
    }
    $raw = sanitize_text_field( wp_unslash( $_GET['ms_chat_state'] ) );
    return rank_ms_chat_verify_state( $raw ); // returns resourceId, returnUrl, exp, etc.
}
```

Verification can delegate to jetforce-wp `POST /api/ms-chat/verify-state` or verify locally if `MS_CHAT_LINK_SIGNING_SECRET` is available via secure config (prefer server-side verify endpoint to avoid secret in WP).

**Recommended:** WP plugin calls jetforce-wp to verify state; does not hold signing secret.

---

## Agent REST API

### Auth middleware

Every `/agent/*` route (except none — all require auth):

```
X-MS-Agent-Secret:   matches RANK_MS_CHAT_AGENT_SECRET (wp-config constant or env)
X-MS-Site-Token:     matches rank_ms_chat_agent_token
X-MS-Resource-Id:    matches rank_ms_chat_resource_id
X-MS-Timestamp:      within 5 min of now
X-MS-Signature:      HMAC-SHA256( timestamp + method + route, agent_token )
```

Also verify `rank_ms_chat_linked_at` is set (connected).

### Routes

| Method | Route | Purpose |
|--------|-------|---------|
| `GET` | `/wp-json/rank-ai/v1/agent/connection/status` | confirmed, version, resource_id |
| `GET` | `/wp-json/rank-ai/v1/agent/health/job-status` | Action Scheduler batch state |
| `POST` | `/wp-json/rank-ai/v1/agent/health/fix/start` | Start health fix batch |
| `GET` | `/wp-json/rank-ai/v1/agent/page-booster/status` | Booster job state |
| `POST` | `/wp-json/rank-ai/v1/agent/page-booster/start` | Start booster |
| `POST` | `/wp-json/rank-ai/v1/agent/page-booster/push-live` | Push booster changes live |
| `POST` | `/wp-json/rank-ai/v1/agent/writer/upload` | Upload article to WP |

Existing public endpoint stays unchanged:

```
GET /wp-json/rank-ai/v1/status → { "status": "Activated" }
```

### Route registration

```php
// includes/ms-chat/class-rank-ms-chat-agent.php
add_action( 'rest_api_init', array( 'Rank_Ms_Chat_Agent', 'register_routes' ) );
```

---

## Services refactor

Extract mutation logic from AJAX into shared services so admin UI and agent REST use the same code path.

| Service method | Source (current) | Used by |
|----------------|------------------|---------|
| `Rank_Ms_Chat_Services::health_fix_start()` | `Rank_Ajax::ajax_start_processing` | AJAX + agent |
| `Rank_Ms_Chat_Services::health_job_status()` | `Rank_Ajax::ajax_get_batch_state` | AJAX + agent |
| `Rank_Ms_Chat_Services::booster_start()` | Rank Booster AJAX | AJAX + agent |
| `Rank_Ms_Chat_Services::booster_status()` | Rank Booster AJAX | AJAX + agent |
| `Rank_Ms_Chat_Services::booster_push_live()` | Rank Booster AJAX | AJAX + agent |
| `Rank_Ms_Chat_Services::writer_upload()` | Rank Writer AJAX | AJAX + agent |

Agent endpoints validate auth then call services. AJAX handlers keep nonce checks then delegate to same services.

---

## Implementation phases

### Phase A — Registration (3 pts)

| Task | File |
|------|------|
| New `Rank_Ms_Chat` class | `includes/ms-chat/class-rank-ms-chat.php` |
| `rank_get_plugin_version()` helper | `includes/config.php` |
| Registry URL constant/config | `includes/ms-chat/class-rank-ms-chat.php` |
| Register on key save | hook in `class-rank-ms-chat.php` |
| Register on activation | `rank_activate_plugin` in `rank-main.php` |
| Unregister on deactivation | `rank_deactivate_plugin` |
| Daily cron | `rank-main.php` |
| Load class | `rank-main.php` |

---

### Phase B — Connect UI + deep link (5 pts)

| Task | File |
|------|------|
| Connect section HTML | `admin/admin-page.php` |
| Connect/disconnect AJAX handlers | `includes/ms-chat/class-rank-ms-chat.php` |
| State parse on page load | `includes/ms-chat/class-rank-ms-chat.php` |
| State verify (call jetforce-wp) | `includes/ms-chat/class-rank-ms-chat.php` |
| Connect JS (redirect on success) | `admin/js/ms-chat-connect.js` |
| Enqueue script on api_config tab | `includes/class-rank-admin.php` |
| Connect section CSS | `admin/css/admin.css` |
| Register AJAX actions | `class-rank-ms-chat.php` |

---

### Phase C — Agent REST auth (3 pts)

| Task | File |
|------|------|
| `Rank_Ms_Chat_Agent` class | `includes/ms-chat/class-rank-ms-chat-agent.php` |
| HMAC verification | `class-rank-ms-chat-agent.php` |
| `GET /agent/connection/status` | `class-rank-ms-chat-agent.php` |
| Agent secret config | `wp-config.php` constant or filter `rank_ms_chat_agent_secret` |

---

### Phase D — Services refactor (5 pts)

| Task | File |
|------|------|
| `Rank_Ms_Chat_Services` class | `includes/ms-chat/class-rank-ms-chat-services.php` |
| Extract health fix start | from `class-rank-ajax.php` |
| Extract health job status | from `class-rank-ajax.php` |
| Extract booster methods | from `class-rank-booster-ajax.php` |
| Extract writer upload | from `class-rank-writer-ajax.php` |
| Update AJAX to delegate | existing AJAX files |

---

### Phase E — Agent endpoints (5 pts)

| Task | File |
|------|------|
| `GET /agent/health/job-status` | `class-rank-ms-chat-agent.php` |
| `POST /agent/health/fix/start` | `class-rank-ms-chat-agent.php` |
| `GET /agent/page-booster/status` | `class-rank-ms-chat-agent.php` |
| `POST /agent/page-booster/start` | `class-rank-ms-chat-agent.php` |
| `POST /agent/page-booster/push-live` | `class-rank-ms-chat-agent.php` |
| `POST /agent/writer/upload` | `class-rank-ms-chat-agent.php` |

---

## New files summary

```
includes/ms-chat/
  class-rank-ms-chat.php          # Registration, connect/disconnect, cron
  class-rank-ms-chat-agent.php    # REST routes + auth
  class-rank-ms-chat-services.php # Shared mutation logic

admin/js/
  ms-chat-connect.js              # Connect button + redirect

docs/
  ms-chat-wp-plugin-implementation.md   # this file
  ms-chat-msio-implementation.md        # msio companion
```

---

## Config constants

```php
// Prefer wp-config.php or environment — not committed secrets
define( 'RANK_MS_CHAT_REGISTRY_URL', 'https://rank.morningscore.io' );
define( 'RANK_MS_CHAT_AGENT_SECRET', '...' ); // same as MS_CHAT_AGENT_SECRET
```

Or via filter for flexibility:

```php
add_filter( 'rank_ms_chat_registry_url', fn() => 'https://rank.morningscore.io' );
add_filter( 'rank_ms_chat_agent_secret', fn() => getenv( 'MS_CHAT_AGENT_SECRET' ) );
```

---

## Testing checklist

### Registration

- [ ] Saving API key triggers register POST
- [ ] Daily cron fires register (or dedup skips when unchanged)
- [ ] Deactivation sends unregister
- [ ] Dedup skips identical fingerprint within 20h

### Connect flow

- [ ] Connect without state: links agent, shows success, no redirect
- [ ] Connect with valid `ms_chat_state`: redirects to `returnUrl?linked=true`
- [ ] Invalid/expired state: error message, Connect blocked
- [ ] Non-admin cannot connect
- [ ] Disconnect revokes link and clears options

### Agent REST

- [ ] Requests without valid HMAC rejected (401)
- [ ] Requests without agent token rejected
- [ ] Connected site: job status returns batch state
- [ ] Health fix start creates batch (same as admin AJAX)
- [ ] Disconnected site: all agent routes return 403

### Regression

- [ ] Existing admin AJAX flows still work after services refactor
- [ ] `/wp-json/rank-ai/v1/status` still public

---

## Out of scope (v1)

- Health issue counts endpoint (ms-chat uses Morningscore API)
- Per-WPML-domain connect
- Holding `MS_CHAT_LINK_SIGNING_SECRET` in plugin (verify via jetforce-wp)
- Outbound command polling for firewalled sites (v2)
- Application Passwords / WP core OAuth

---

## Definition of done (WP plugin)

- [ ] Plugin registers with jetforce-wp on key save, activation, and daily
- [ ] Connect/disconnect UI on API key tab
- [ ] Deep link from ms-chat opens connect page with signed state
- [ ] After deep-link connect, redirects to ms-chat `returnUrl?linked=true`
- [ ] Agent token stored in WP only; central gets hash
- [ ] Agent REST routes secured with HMAC + service secret
- [ ] Health fix, booster, writer mutations work via agent endpoints
- [ ] AJAX admin flows unchanged for end users

---

## Build order (suggested)

1. **Phase A** — registration (unblocks msio discover testing)
2. **Phase B** — connect UI + deep link (unblocks end-to-end link flow)
3. **Phase C** — agent auth skeleton + connection/status endpoint
4. **Phase D** — services refactor (do alongside E per feature)
5. **Phase E** — agent endpoints one feature at a time (health → booster → writer)

Can run in parallel with msio Phase A once registry URL is agreed.
