|
1 | 1 | ## Last session |
| 2 | +2026-08-27 (**the WordPress.org round-2 email closed out end to end: the finding they raised answered where a re-scan reads it, the same class of gap swept one step further, and the `.org` zip rebuilt and verified in the artifact**) — Issues [#150](https://github.com/plugpressco/saddle/issues/150) and [#161](https://github.com/plugpressco/saddle/issues/161) closed; PRs [#160](https://github.com/plugpressco/saddle/pull/160) and [#162](https://github.com/plugpressco/saddle/pull/162) merged. **653 tests** (was 645), 0 lint errors. `dist/saddle-1.0.0.zip` rebuilt, 664,345 bytes, sha256 `d6923270…`, 103 files. **Plugin Check against the extracted zip staged as `saddle`: 0 errors**, the same 3 pre-existing warnings. **Not uploaded by me.** |
| 3 | + |
| 4 | +**What was actually outstanding, as opposed to what the email said.** Both findings had been *worked* on 2026-08-25 and neither loop had been *closed*: the reply was drafted and never sent, and `dist/saddle-1.0.0.zip` was built at 14:05 that day while #158 landed at 17:17 — so the zip on disk carried neither the current accent CSS nor the current changelog. Re-derived against the tree first rather than trusting the log: `require_readable_post()` is in place and reached from all nine id-taking reads, `collection()` filters the four listings, and `get-preview-url` does its own stricter check. The fix was real. It was just invisible at the line the reviewer quoted. |
| 5 | + |
| 6 | +**The reviewer quoted the registration line; the fix is on the execute path.** That is the whole reason a round 3 was likely on an unchanged resubmission — an automated scan reads `permission_callback => Saddle_Capabilities::permission( 'read', 'read', 'get-media' )` and nothing at that line says the object is authorized elsewhere. So the two-layer model is now stated at the top of `core-content.php` and pointed at from every id-taking read and every listing. It also records **why** the object check is not in the gate, which was previously only implicit: core does pass `$input` to `WP_Ability::check_permissions()` and accepts a `WP_Error` back, so it could move — but `denial_reason()` and `is_callable_now()`, which build `tools/list` and every refusal an agent reads, are input-free by construction, and a gate that says "no" without naming which layer said so is what puts an agent into a retry loop. `get-preview-url`'s comment records that it is deliberately **stricter** than the funnel (an unpublished post needs `edit_post`, not `read_post`), not outside it. |
| 7 | + |
| 8 | +**The Unsplash 401 is a false positive and nothing in this plugin can clear it.** Reproduced again: `unsplash.com` is behind Anubis — `307 → /.within.website?redir=…` then `401` to any UA containing `Mozilla`, `200` to `curl`. The part that decides the answer is that it is **not** limited to the flagged path: `/api-terms`, `/privacy`, `/terms`, `/documents/*` and `/license` all behave identically, so swapping the flagged URL fixes nothing and there is no browser-reachable canonical Unsplash terms or privacy URL to swap to. Only `help.unsplash.com` answers 200 to a browser, and it already leads the disclosure. Both canonical links stay; §14 carries the reproduction as a table. |
| 9 | + |
| 10 | +**#150 closed, and the naive fix would have been wrong twice.** `recall-changes` and the system context's "recent changes" section both returned the mutation log unfiltered, and each row's `summary` carries the title of what changed — so a Subscriber-level credential could enumerate the titles of drafts it can never open. The filter went into `Saddle_Log::recent_executed()`, not the ability, because the context path is the one that reaches **every** connected session. Two edges that a straight `read_post` check gets wrong: a row whose post no longer exists fails `read_post` for everyone (`map_meta_cap` denies a missing post), which would have **erased deletion history from the owner's own log** — it now survives for an account that can delete content; and `target` is a bare id with no type, so a `create-tag` row's term id is judged as a post id — over-filtering a public taxonomy row for a low-privilege caller, never disclosing anything, and recorded as the accepted cost rather than left as a surprise. |
| 11 | + |
| 12 | +**One test change worth knowing about.** `Saddle_Context_Test` never set a current user, so it ran as user 0 — and the new filter correctly withholds every row naming a post from an unauthenticated caller, which turned three folding tests red. It now runs as an administrator, which is what the system context is always assembled for in the field. The suite was not weakened to accommodate the filter; the tests were made to match the real path. |
| 13 | + |
| 14 | +**Verified on the live install at localhost:8882**, not only in the suite: a real `create-post` through the real ability produced a real log row, then admin saw 20 rows including the draft's title and the subscriber saw 19 without it, `read_post` false on the draft, and the system context named the draft for the admin and not for the subscriber. Fixtures fully removed afterwards — the draft, its log row, both Application Passwords **deleted by uuid** (not `--all`, which is what wiped every admin credential last time), and the throwaway subscriber. |
| 15 | + |
| 16 | +**The zip, verified in the artifact rather than the tree:** `Version: 1.0.0` and `SADDLE_VERSION` `1.0.0` (temporary uncommitted header, tree restored to `1.0.0-rc9` afterwards and confirmed clean), `Stable tag: 1.0.0`, updater **absent**, no `class-saddle-ecosystem.php`, `includes/lib` 0 files, no `tests/`/`dist/`/`*.md`/dotfiles, the #158 accent present in `admin/build/style-index.css`, the #150 filter and the two-layer note both present, all 103 PHP files parse, zero hits for the six execution functions (the five `system(` matches are the phrase "design system (" in prose), zero `error_log`/`var_dump`/`print_r`/`fwrite`, and five `wp_remote_*` sites matching the documented four classes with **no updater call**. |
| 17 | + |
| 18 | +**Two things to know before the next session.** |
| 19 | + |
| 20 | +- **CI is red on both merged PRs, on the inherited [#145](https://github.com/plugpressco/saddle/issues/145) failure alone** — `Saddle_Skills_Test::test_the_playbook_adapts_step_two_to_a_classic_theme`, CI-only, predating both branches. Locally 653/653. Merged with that stated, not written over. |
| 21 | +- **The MCP endpoint is not registered over HTTP on the divi-dev install**, which is why the live check drove the abilities directly instead of the transport. It is a dev-tree artifact, not a shipped bug: that tree still contains `includes/lib/wp-mcp`, so `adapter_available()` is true and Saddle correctly steps aside — but no MCP Adapter *plugin* is active to register the route, so nobody does. Both zips exclude `includes/lib/**`, so this condition cannot occur in the field. Worth a guard someday; not a blocker. |
| 22 | + |
| 23 | +**Next up:** Fahim uploads `dist/saddle-1.0.0.zip` at "Add your plugin" and sends the reply drafted at the end of `WPORG-SUBMISSION.md` — re-run the two `curl` commands in §14 that day so the transcript is current. Still open from before: point the Codex tester at rc9 and get their **Connection details → Transport** line; Saddle Pro has an unreleased License-card change on its `main` and is still tagged 1.6.0; [#159](https://github.com/plugpressco/saddle/issues/159) (OAuth discovery never advertises `offline_access`) is open. Nothing self-hosted may be published from this tree without a bump — `main` is at `1.0.0-rc9` and rc9 is already live. |
| 24 | + |
| 25 | +## Previous session |
2 | 26 | 2026-08-25, later still (**a Codex tester's handshake failure root-caused to the adapter transport, fixed, and rc9 built**) — Issue [#155](https://github.com/plugpressco/saddle/issues/155) / PR [#156](https://github.com/plugpressco/saddle/pull/156) merged. **645 tests** (was 637), 0 lint errors. `dist/saddle-1.0.0-rc9-selfhosted.zip` built and verified, **NOT published to R2** — Fahim's call, same as rc8. |
3 | 27 |
|
4 | 28 | **The report, and why the obvious answer was wrong.** A tester could not connect with Codex: `initialize` 200, then `notifications/initialized` **200 with an empty body**, then `EOF while parsing a value` — which is precisely what a strict client does when it tries to JSON-parse nothing. The spec is unambiguous that a notification gets 202 with no body, and that is the step sitting between "connected" and `tools/list`. |
|
0 commit comments