Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions STATUS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
## Last session
2026-08-20/21 (**Mark found it. A pre-init fatal in the OAuth bearer resolver — the likely root cause of the whole two-week kesuk.net thread — fixed, rc8 built for his retest, .org submission zip built**) — Mark enabled WP_DEBUG and sent the stack trace; it was exactly right. Issue [#127](https://github.com/plugpressco/saddle/issues/127) filed and closed; PRs [#128](https://github.com/plugpressco/saddle/pull/128) (fix), [#129](https://github.com/plugpressco/saddle/pull/129) (rc8 bump), [#130](https://github.com/plugpressco/saddle/pull/130) (Plugin Check annotation) merged. **612 tests** (was 603), 0 lint errors.

**The bug, and why only one of his two sites had it.** `Saddle_OAuth_Bearer::resolve()` sits on `determine_current_user`, registered at include time. Any plugin calling `is_user_logged_in()` during `plugins_loaded` — AIOSEO Pro does; with it deactivated, Internal Links Premium does — fires that filter **before wp-settings.php creates `$wp_rewrite`**. On a genuine ChatGPT MCP request (Bearer header present, `saddle/v1/mcp` URI, valid token with a stored RFC 8707 `resource`) every guard passes and the audience check at `class-saddle-oauth-bearer.php:122` calls `resource_id()` → `rest_url()` → fatal at `wp-includes/rest-api.php`. The crash ate the OAuth challenge, so ChatGPT never retried with its token — Mark's endless `refused (401)` rows. mrr.org.uk has no early-asking plugin; that is the *entire* difference between his working and failing sites. **The repo already knew this window existed**: `Saddle_OAuth_Store::find()` uses raw `$wpdb` with a docblock saying the resolver can fire before `init` — the DB half was hardened, the URL half three lines later was not. Latent since the OAuth server landed (`includes/oauth/` untouched since `0c3b266`); the site's plugin mix, not any rc, decided who hit it. The rc7 bearer probe (#123) could never reproduce it — it targets `auth-probe`, and `targets_mcp_endpoint()` short-circuits before the fatal line.

**The fix, and the constraint that shaped it.** `resource_id()` now routes through `rest_url_early()`: rewrite global present → defer to core untouched; absent → replicate `get_rest_url( null, $path, 'rest' )` **byte-identically**, because the value is compared against the `resource` stored on every access token at authorize time — divergence would refuse every token as `invalid_token`, trading a fatal for an invisible auth break. The replica is provably identical: core needs exactly two things from `$wp_rewrite` — `using_index_permalinks()`, a pure regex on the `permalink_structure` option, and `WP_Rewrite::$index`, hard-coded `'index.php'`; anything customizing `$index` does so on the real instance, which only exists once the branch defers to core anyway. Rejected: skipping auth pre-init (core memoizes user 0 for the whole request — same dead connector, quieter) and injecting the global early (wp-settings.php overwrites it; other plugins' `add_rewrite_rule` calls would be silently eaten). `issuer()`/`endpoint()` left on plain `rest_url()` — a full audit found bearer.php:122 is the **sole** pre-init-reachable `rest_url()` in the OAuth subsystem.

**Tests: red first, and the identity pinned.** Three regressions committed failing with Mark's exact stack (`before_wp_rewrite_exists()` helper, try/finally because `WP_UnitTestCase` never restores that global): resolve works early; `resource_id()` identical before/after init on pretty, **index** and plain permalinks (must use `set_permalink_structure()`, not bare `update_option()` — a stale live instance makes the comparison lie); early is not an excuse to skip the audience check. The gap that let this ship: all 25 prior bearer tests ran `resolve()` fully booted.

**Reproduced for real, both directions.** WP Playground + mu-plugin firing `is_user_logged_in()` at `plugins_loaded` prio 5 (after `recover_auth_header` at 1, matching Mark's site) + seeded token: `main` → **500** with his stack line for line; fix branch → **200**, full 40-tool read-tier list; no token → correct 401 + `WWW-Authenticate` discovery challenge; without the mu-plugin → 200 (post-init path unchanged). Existing tokens stored the real-`rest_url()` value and the replica reproduces it exactly, so his connection should recover **without re-consent**.

**Builds, both channels, verified in the zips.** `saddle-1.0.0-rc8-selfhosted.zip` (updater present, rc8 in both header sites, fix present, no dev files) and `saddle-1.0.0.zip` for the .org submission (updater **absent**, stable tag 1.0.0, built from a temporary uncommitted 1.0.0 header — main stays at rc8). `revendor-wp-mcp.php --check` clean. **Plugin Check run against the built .org zip** (extracted-zip copy swapped in for the plug-press symlink, then restored — never the symlinked dev tree): one ERROR, `Tested up to: 7.0 < 7.1`, which is **Fahim's bump to make** (note: this session's suite and Playground runs all executed on WP 7.1, green); two pre-existing warnings (vendored adapter hook in mcp-compat, dynamic hooks in the integration engine); my new `apply_filters( 'rest_url', … )` warning annotated away in #130. **The rc8 zip predates #130 by one comment-only commit — deliberately not rebuilt** (never two zips under one number).

**Delivery state: NOT published to R2** — Fahim chose direct delivery over the channel (publishing rc8 would offer it to every self-hosted install, not just Mark). Both zips handed to Fahim in-session. **Gmail reply drafted** (standalone draft, subject "Saddle 1.0.0-rc8 — your diagnosis was right…") — Mark's thread is not in the connected Gmail account, so Fahim moves the text into the real thread, attaches the rc8 zip, and sends. The draft credits his debugging explicitly; he earned it.

## Previous session
2026-08-18, later (**Mark's traffic capture killed my own diagnosis, and exposed two instruments that could not answer the question they exist for → rc7, LIVE on the channel**) — Fahim forwarded a clean ChatGPT reconnection recorded with the client-traffic recorder running. Issues [#120](https://github.com/plugpressco/saddle/issues/120) and [#121](https://github.com/plugpressco/saddle/issues/121) filed and closed, PRs [#122](https://github.com/plugpressco/saddle/pull/122)/[#123](https://github.com/plugpressco/saddle/pull/123)/[#124](https://github.com/plugpressco/saddle/pull/124) merged, **603 tests** (was 596), 0 lint errors. **rc7 published to R2 and verified live.**

**Retraction, and it was mine.** Earlier today I told Fahim the MCP-Adapter shim chain (#111/#112) was Mark's bug. It is not. His own report says `Transport: Saddle built-in JSON-RPC` — he does not have that plugin, so that code path never runs on his site. rc6 was worth shipping and was never his fix. *(Second correction in one day: I also wrote here that rc5 was never published. It was — the live channel was serving it. Both errors came from asserting before checking, once against a stale git ref and once against a file I had not read.)*
Expand Down Expand Up @@ -308,21 +323,15 @@ Earlier 2026-07-12 — **Audit-backlog execution (P0–P4)**. Cleared the audit
- Earlier same day: the Closed-Loop Quality Engine free half (epic #22, PRs #28–#32, 300 green) — see git history.

## Next up
- **Mark Roach — reply drafted, awaiting Fahim's send. Two retractions in it.** (1) The MCP-Adapter diagnosis was WRONG: his own report says `Transport: Saddle built-in JSON-RPC`, so he does not have that plugin and rc6's headline fix is not his fix. (2) The trace shows ChatGPT's client (`Python/3.12 aiohttp/3.13.5`) refused **401 at the transport gate**, twice, with no `initialize` and no `tools/list` in the window — so this is authentication, full stop, not discovery, schemas, tiers or the handshake. Ask him for the **one** thing that closes it: run the Bearer curl probe below, then re-read Client traffic. rc7 now shows `auth:` and `scheme:` on every row, so the answer is readable without another round trip.
```bash
curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://staging.kesuk.net/wp-json/saddle/v1/mcp \
-H 'Content-Type: application/json' -H 'Authorization: Bearer saddle-probe-not-a-real-token' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
```
`scheme:bearer` on that row → the header arrives, ChatGPT is not sending its token. `scheme:none` → 20i is stripping Bearer, and Connections → Connection details & health will now say so itself and offer the one-click rule. **Still ask him to rotate the mrr.org.uk credentials he emailed in plaintext.**
- **rc7 is LIVE on the self-hosted channel** — `/v1/update` returns 1.0.0-rc7 with a signed package, and the zip served through the live link is byte-identical to the local build (sha256 `8ea73d41…`, 651,504 bytes) and **contains the updater** (verified by unzipping what the URL returns, not what was uploaded). First rc to actually reach anyone since rc5.
- **Confirm which zip Mark installed.** `class-saddle-updater.php` ships only in `-selfhosted`; if he has the `.org` zip his site never checks for updates and rc7 will not reach him either. One more manual install and then never again.
- **The bearer probe is stubbed, not field-proven.** `test_self_check_catches_a_bearer_stripped_while_basic_survives` drives a loopback that answers differently per scheme. Mark's site is the live test. If his report says `scheme:bearer`, the probe is correct and the problem is OpenAI's side of the token exchange — record that here either way.
- **Send Mark the rc8 zip.** The Gmail draft is written (his thread is not in the connected account, so move the text into the real thread), attach `saddle-1.0.0-rc8-selfhosted.zip`, send. His retest — same ChatGPT connection test, AIOSEO Pro left active — is the field confirmation for #127; **record his result here either way.** The connection should recover without re-consent (tokens store the value the replica reproduces exactly); if it doesn't, that is a finding, not noise. **Still ask him to rotate the mrr.org.uk credentials he emailed in plaintext.**
- **Decide whether rc8 goes to R2.** It exists as a zip only; the channel still serves rc7. Publishing (`./scripts/release.sh saddle 1.0.0-rc8 <zip> <manifest>`) offers the fix to every self-hosted install — probably right once Mark confirms, premature before.
- **`Tested up to: 7.1` — Fahim's call, and now the only Plugin Check ERROR on the built .org zip.** WP 7.1 shipped 2026-08-19. Evidence for the bump already exists: this session's full suite (612 green) and both Playground verifications ran against 7.1. Bump readme.txt after Fahim confirms he's satisfied, then rebuild the submission zip.
- **The bearer probe (#123) is still stubbed, not field-proven — and now expects a different answer.** With the #127 fatal fixed, Mark's Client traffic should finally show `scheme:bearer` rows reaching the resolver. If his retest still shows `nothing sent`, the 20i edge really is stripping Bearer and the probe's one-click rule is the next move.
- **Migrate saddle-pro and mailyard onto `saddle_context_sections`.** Free's half shipped in #102 and both still append through the old string filter, so Pro's `##` heading and Mailyard's floating "For email delivery problems:" sentence are still in the live context. Pro's tree currently has uncommitted release prep in it (a 1.4.0 → 1.4.1 bump), which is why it was left alone. Nothing breaks meanwhile — the compat path is tested.
- **The three surviving branches are all superseded or dead — decide and delete.** `feat/admin-header` (9 commits) and `feat/ui-plugpress-ui-v0.6.0` (a strict subset of it) targeted `@plugpress/ui` v0.8.2; `main` is on **v0.11.3**, TopBar and `Saddle_Log::record_action()` are both there, and every file they add exists on main in restructured form. `feat/freemius-parent` puts Freemius in FREE, which contradicts both CLAUDE.md ("no licensing or upsell in free") and the decision that free ships on WP.org with no Freemius.
- **Delete the v1.1.0 GitHub release + tag (one command):** `gh release delete v1.1.0 --repo plugpressco/saddle --cleanup-tag --yes && git tag -d v1.1.0`. Still outstanding, and #69 tried to reintroduce 1.1.0 this session — reverted in four places, including a `Stable tag` that was not a conflict and would have shipped silently.
- **Run `php scripts/revendor-wp-mcp.php --check` before any release.** New pre-release guard: it exits non-zero if the vendored library was refreshed without re-applying Saddle's text domain.
- **SUBMIT to WordPress.org.** Everything code-side is done and this session removed one of the reviewer-facing surfaces (#86). Fahim's three steps are in `WPORG-SUBMISSION.md`. Rebuild first: `npm run build && npx grunt build` never `grunt release`, it bumps the version.
- **SUBMIT to WordPress.org.** `dist/`-equivalent `saddle-1.0.0.zip` is built and Plugin Check-verified (sole ERROR is the Tested-up-to bump above; two pre-existing warnings). Fahim's three steps are in `WPORG-SUBMISSION.md`. If rebuilding: the tree sits at 1.0.0-rc8 — set the header to 1.0.0 temporarily, `npm run build && npx grunt build`, restore; never `grunt release`.
- **Live ChatGPT round-trip on plugpress.co** — still the real gate for the OAuth work, and now carrying three questions at once: the handshake fix against the client that exposed it, [#108](https://github.com/plugpressco/saddle/pull/108)'s level picker (set the existing connection to "Managing the site" on Connect, refresh the connector, expect ~160 tools and `saddle-waggle-update-seo-meta` present), and the plan question — if the write tools are *listed* and ChatGPT still declines to call them, that is the Business/Enterprise/Edu gate, not us. Record the answer in this file either way.
- **CI PHPUnit still red on GitHub Actions** (no WP core in the runner). Worth its own PR so future PRs get a real green; "green" currently means someone ran the suite locally and said so.

Expand Down
Loading