Skip to content

Phase 3: DRY — shared integration engine, accessor resolver, gate registry - #68

Merged
ifahimreza merged 4 commits into
mainfrom
improve/phase3-dry
Aug 16, 2026
Merged

Phase 3: DRY — shared integration engine, accessor resolver, gate registry#68
ifahimreza merged 4 commits into
mainfrom
improve/phase3-dry

Conversation

@ifahimreza

Copy link
Copy Markdown
Contributor

Closes #64. Stacked on #67 (Phase 2). Pro-side counterpart: saddle-pro PR from the same phase.

  • Saddle_Integration_Engine — one wrap/executor implementation for both catalogs, including the deferred Phase-2 items: full-args destructive bind, merged target keys, force_destructive override.
  • Saddle_Accessors — single accessor resolution for lint/render/verify (3 copies removed); verify's count(skipped)>=3 magic number replaced.
  • Gate registry: denial_reason() now reads the exact (level, cap) permission() enforces, in the same order, with the missing capability reason added.
  • require_readable_post() helper; merged theme.json preset resolvers.

3 new engine tests (bind replay, object-cast schema normalization, force_destructive); suite green (368 tests). Pro suite green against the shared engine.

🤖 Generated with Claude Code

https://claude.ai/code/session_0133CZhoFPY6BBChDDGEa22Q

ifahimreza and others added 3 commits July 24, 2026 01:48
…llisions

- Saddle_Approval::gate now logs a confirmed destructive execution even when
  the executor returns WP_Error — a partial mutation with no audit trail is
  worse than a noisy one. The entry carries a "FAILED after confirmation"
  marker plus the error message.
- Saddle_Integrations::wrap surfaces genuine wrapper-name collisions via
  _doing_it_wrong instead of silently dropping the partner tool; its own
  wrappers are tracked so idempotent re-runs stay silent.

Part of Phase 1 (correctness) of the 2026-07 architecture review
(plugpressco/saddle-pro#39).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0133CZhoFPY6BBChDDGEa22Q
…it log caps, domain enforcement

- Approval tokens are bound to the previewing user: with several agents on
  one site, agent A's preview can no longer be confirmed by agent B
  (saddle_token_user_mismatch).
- Saddle-issued app passwords are recorded by UUID in user meta
  (saddle_issued_credentials); credential scoping, the XML-RPC block, and
  client revoke/rotate/list key on that immutable marker instead of the
  user-editable display name. Legacy prefix-named keys migrate on sight.
- Saddle_Log GC caps denials (300) and executed mutations (1000) as
  separate filterable buckets, batch raised to 500, so denial noise can
  never evict change history.
- Opt-in domain-drift enforcement (saddle_enforce_tier_domain, default
  off): write/admin abilities refuse on a migrated/cloned domain until the
  owner re-confirms the tier; exposed via GET/POST /settings.

Phase 2 of the 2026-07 architecture review (#63).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0133CZhoFPY6BBChDDGEa22Q
…, small dedupes

- New Saddle_Integration_Engine: the wrap/executor safety logic that
  existed as ~280 near-identical lines in free and Pro now lives once in
  free. Saddle_Integrations is catalog + filter names + context shape.
  The engine also lands the Phase-2 hardening deferred to it: full-args
  `bind` on wrapped destructive tools (a confirm can no longer change
  arguments after preview), merged target key list, and a
  force_destructive catalog override for partners that forget the
  destructive annotation.
- New Saddle_Accessors: one build→filter→instanceof→409 resolution for
  lint-page / render-node / verify-page (was three hand-synced copies);
  verify's magic count(skipped)>=3 heuristic replaced with the explicit
  resolver verdict. Error codes and message strings unchanged.
- Saddle_Capabilities records each ability's enforced (level, cap) at
  permission() build time; denial_reason() now mirrors the closure's
  exact order, gains the previously-missing capability reason, and reads
  the enforced tier instead of re-deriving from meta.
- Saddle_Abilities::require_readable_post() replaces the copy-pasted
  read preamble; the two theme.json preset resolvers in the Gutenberg
  lint accessor merge into one preset_map().

Phase 3 of the 2026-07 architecture review (#64). record()/record_action()
was reviewed and intentionally left split: record() is the sole entry
carrying `type`, which denial logging needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0133CZhoFPY6BBChDDGEa22Q
Three weeks of main landed on top of this branch, and two changes collided
head-on with it rather than beside it.

**The wrapper bind.** Both this branch and #90 fixed the same hole — a
destructive partner tool could be confirmed with different arguments than its
preview showed — and they fixed it differently. This branch's extracted engine
binds `substr( md5( json ), 0, 12 )` of the raw argument array. #90 binds a
full sha256 over a recursively key-sorted copy. The sorting is the part that
matters: without it, a client that serializes the same arguments in a
different key order has a LEGITIMATE confirm refused. Kept this branch's
structure (the engine owns the executor now) and ported #90's hash and its
widened, filterable target-key list into it.

**denial_reason().** This branch rewrote it to read the same recorded gate the
closure enforces instead of re-deriving the tier from ability meta, which is
strictly better and is what `$gates` is for — so `required_cap()` from #90 now
reads that registry rather than keeping a second map that could drift. Two
things were restored on top: the capability message names the capability
(an agent told "you lack a permission" without being told WHICH cannot relay
it), and the OAuth insufficient-scope branch, which distinguishes "this site
does not allow that" from "this app was granted less than the site allows" —
different screens, different fixes, and oauth-bearer-test.php pins it.

One test changed meaning rather than breaking: it asserted that a disabled
tool outranks a capability denial. permission() checks the capability FIRST,
so that was simply wrong, and it only surfaced once denial_reason() started
mirroring permission()'s real order. Rewritten to pin the ordering in both
directions.

Also fixed a docblock this branch shipped with, and one my own insertion split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkZr73cqaSesHRDG89Yy8S
@ifahimreza
ifahimreza merged commit fb67a4b into main Aug 16, 2026
1 of 7 checks passed
@ifahimreza
ifahimreza deleted the improve/phase3-dry branch August 16, 2026 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DRY: shared integration engine, accessor-resolution helper, single gate-order source

1 participant