Skip to content

fix: align destructive gates across tool planes - #13

Closed
mechkw wants to merge 2 commits into
GravityKit:developfrom
mechkw:feat/consistent-destructive-guardrails
Closed

mechkw wants to merge 2 commits into
GravityKit:developfrom
mechkw:feat/consistent-destructive-guardrails

Conversation

@mechkw

@mechkw mechkw commented Aug 26, 2026

Copy link
Copy Markdown

Summary

Align destructive-operation safety across the MCP server's two tool planes while preserving the autonomous, single-credential workflow:

  • gate gf_delete_field with the same GRAVITY_FORMS_ALLOW_DELETE policy already used for form, entry, and feed deletion;
  • gate destructive dynamic GravityKit abilities locally as well as through their WordPress permission_callback;
  • propagate WordPress Ability safety annotations to MCP ToolAnnotations, so clients can correctly distinguish read-only, mutating, destructive, and idempotent tools;
  • let dynamic destructive abilities inherit GRAVITY_FORMS_ALLOW_DELETE, with an optional GRAVITYKIT_ALLOW_DELETE override for deployments that need separate policy.

Why

The previous behavior had two inconsistent paths:

  1. gf_delete_field replaced the complete form without consulting api.allowDelete, even though every other static delete operation required the flag.
  2. Dynamic tools discarded the Ability API's annotations and relied only on server-side permissions. MCP clients therefore could not classify those tools, and the local delete policy did not cover destructive dynamic abilities.

For autonomous operators, the clean model is one explicit full-authority switch plus rollback-first execution—not per-object credential swapping or misleadingly incomplete guardrails. For conservative deployments, deletes remain off by default and the product-specific override can be set independently.

Compatibility

  • No tool names, input schemas, routes, credential formats, or non-destructive behavior change.
  • Existing deployments with GRAVITY_FORMS_ALLOW_DELETE=true retain delete access across both tool planes.
  • GRAVITYKIT_ALLOW_DELETE is optional; when omitted it inherits the existing Gravity Forms flag.
  • Destructive dynamic abilities are now blocked when both flags are false. This is the intended security hardening for the previously uncovered path.
  • WordPress capability checks and Foundation per-ability enable/disable controls remain in force.

Verification

  • npm run prepublishOnly — passed
    • unit suites
    • Node test suite
    • field validation
    • GravityView tests
    • package lint
    • documentation freshness
  • Added regression coverage for:
    • blocked field deletion performing no write;
    • field-delete tool description advertising the gate;
    • dynamic Ability annotation normalization;
    • environment-policy inheritance/override;
    • blocked destructive abilities never reaching WordPress;
    • explicitly allowed destructive abilities retaining the correct HTTP method.
  • Packaged the branch and completed a real stdio MCP handshake through Hermes against a local WordPress/Gravity Forms mock: 27 tools discovered.

Operational note

Snapshots, readback diffs, browser QC, and automatic rollback are orchestration concerns because the MCP server cannot infer the user's requested task boundary or guarantee restoration of every WordPress-side effect. This patch deliberately keeps that policy out of core while ensuring the server's local guardrails and MCP metadata are truthful and consistent.

cc @zackkatz — this is meant as a small, compatibility-oriented hardening patch rather than a broader privilege-model redesign. I would appreciate your view on whether the shared-flag inheritance is the right default for upstream.

Summary by CodeRabbit

  • New Features

    • Added configurable protection for destructive operations, including field deletion.
    • Added support for overriding deletion access through GRAVITYKIT_ALLOW_DELETE, with fallback to the existing Gravity Forms setting.
    • Improved tool metadata and documentation for deletion requirements.
  • Bug Fixes

    • Prevented delete actions from executing or persisting changes when deletion is disabled.
    • Normalized tool annotations for consistent behavior.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 26 days. After that, they cost $0.25 per reviewed file.

Or wait 52 minutes for your next included review.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a4609d2-9ab5-4403-be73-37b88a71ca60

📥 Commits

Reviewing files that changed from the base of the PR and between 3570d10 and a378208.

📒 Files selected for processing (2)
  • src/abilities/loader.js
  • test/abilities-loader.test.js

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23dd3080-b0fb-4c7e-b0c0-d7f6e9a9046b

📥 Commits

Reviewing files that changed from the base of the PR and between 8ecd056 and 3570d10.

📒 Files selected for processing (7)
  • .env.example
  • README.md
  • src/abilities/loader.js
  • src/field-operations/field-manager.js
  • src/field-operations/index.js
  • test/abilities-loader.test.js
  • test/field-manager.test.js

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The change adds GravityKit deletion configuration, normalizes MCP annotations, and gates destructive ability and field deletion execution. Documentation and tests cover configuration inheritance, blocked operations, permitted DELETE requests, and tool metadata.

Changes

Deletion Policy and Ability Execution

Layer / File(s) Summary
Deletion policy and annotation contract
.env.example, README.md, src/abilities/loader.js, test/abilities-loader.test.js
The loader resolves GRAVITYKIT_ALLOW_DELETE with fallback to GRAVITY_FORMS_ALLOW_DELETE. WordPress annotations are normalized into MCP hints. Tests cover policy precedence and annotation conversion.
Destructive ability execution gate
src/abilities/loader.js, test/abilities-loader.test.js
Catalog-generated tools receive allowDelete. Destructive handlers reject execution when deletion is disabled and use DELETE requests when enabled.
Field deletion enforcement and documentation
src/field-operations/field-manager.js, src/field-operations/index.js, README.md, test/field-manager.test.js
deleteField rejects disabled deletion before persistence. Tool descriptions and tests document and verify the ALLOW_DELETE=true requirement.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 3570d

This change aligns destructive-operation gating and tool metadata across the two tool planes while preserving existing non-destructive behavior. The documented checks passed, and no actionable merge-blocking risk remains beyond normal review.

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant loadAbilitiesAsTools
  participant buildTools
  participant WordPressAPI
  MCPClient->>loadAbilitiesAsTools: load catalog tools with allowDelete
  loadAbilitiesAsTools->>buildTools: pass deletion policy
  buildTools->>buildTools: normalize annotations and create handler
  buildTools->>WordPressAPI: dispatch DELETE when deletion is enabled
``

<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->

<details>
<summary>🚥 Pre-merge checks | ✅ 5</summary>

<details>
<summary>✅ Passed checks (5 passed)</summary>

|         Check name         | Status   | Explanation                                                                                                                                                                                               |
| :------------------------: | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|     Docstring Coverage     | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 5 files. (2 skipped: 2 … |
|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                                                                  |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                                                                                  |
|      Description Check     | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                                               |
|         Title check        | ✅ Passed | The title accurately and concisely summarizes the main change: aligning destructive-operation gates across the static and dynamic tool planes.                                                            |

</details>

<details>
<summary>Full details: Docstring Coverage</summary>

**Explanation**

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 5 files. (2 skipped: 2 unsupported.)

</details>

</details>

<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

<details>
<summary>🧪 Generate unit tests (beta)</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with unit tests

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=GravityKit/MCP&utm_content=13)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>


<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>

<!-- tips_end -->
Loading

@mechkw

mechkw commented Aug 26, 2026

Copy link
Copy Markdown
Author

Follow-up hardening in a378208: an independent review caught that WordPress Abilities uses null for unknown annotations. The mapper now omits unknown/null MCP hints instead of converting them to false, preserving MCP's conservative defaults (notably destructiveHint defaults to true when unspecified). Added a regression test for the null/unknown case; targeted ability-loader and field-manager suites pass.

zackkatz added a commit that referenced this pull request Sep 17, 2026
…tated tools

Two safety defaults that were pointed the wrong way.

gf_delete_field never checked allowDelete, while deleteForm, deleteEntry and
deleteFeed all do -- so a server configured to refuse deletions still let an
assistant remove a field, and a deleted field does not go to the Trash the way
a form or entry does. The gate is mechkw's, cherry-picked from PR #13; his
test needed rebasing onto the current mock, which had no allowDelete at all.

The annotation mapping coerced an absent `destructive` to false, publishing
"safe" for an ability that had simply said nothing. The MCP spec defaults an
omitted destructiveHint to true, and WordPress core defaults ability
annotations to null, so this was reachable rather than theoretical. Unstated
now resolves to destructive unless the ability declares readonly. What we
BLOCK still keys on an explicit declaration, so a product that forgets its
annotations looks cautious rather than broken.

One fixture declared `annotations: {}` for a create ability; all 50 real
GravityView abilities declare `destructive` explicitly, so it was corrected
rather than accommodated, and the unknown case got its own coverage for both
null and {}.

Raised in Fable's review of the open PRs.
@zackkatz

Copy link
Copy Markdown
Member

Thanks @mechkw, and sorry this sat so long. Your PR addresses three things. We took one, fixed another because you flagged it, and had already replaced the third.

  1. The field delete gate. Real bug, now in as 2813aed and credited to you in the changelog. deleteForm, deleteEntry and deleteFeed all checked the switch; deleteField didn't — and a deleted field doesn't go to the Trash. Your test needed a rebase: the mock client had no allowDelete on it at all, which is probably how the gap got there.

  2. Null annotations. Right again. An ability that declared nothing came out as destructiveHint: false, where the spec says unset means destructive. Same commit.

  3. The allow-list. This is the one we're closing. We pushed a wider version to develop today: GRAVITYKIT_MCP_ALLOW_DESTRUCTIVE takes all, a product prefix, or an exact tool name, so you can allow migrations without allowing View deletes. GRAVITY_FORMS_ALLOW_DELETE=true still means all. Same lines as yours, so it conflicts now — our doing for pushing straight to develop, not anything you did.

Closing the PR, but two of three shipped with your name on them. Thanks!

@zackkatz zackkatz closed this Sep 17, 2026
@zackkatz zackkatz mentioned this pull request Sep 17, 2026
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.

2 participants