Skip to content

refactor(sdk): ship the destructive-confirmation helpers in pipefy_sdk - #651

Open
mocha06 wants to merge 1 commit into
devfrom
rc-dev/refactor/649-sdk-destructive-confirmation
Open

refactor(sdk): ship the destructive-confirmation helpers in pipefy_sdk#651
mocha06 wants to merge 1 commit into
devfrom
rc-dev/refactor/649-sdk-destructive-confirmation

Conversation

@mocha06

@mocha06 mocha06 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Closes #649.

Motivation

The SDK ships ungated delete_* / remove_* methods — 56 across the package — plus a docstring obligation to gate them, but not the code that does the gating. delete_pipe and delete_table both say "Caller must enforce preview/confirm UX".

The canonical implementation of that protocol lived in pipefy-mcp-server. An application that embeds the SDK in process cannot import it without taking two things it does not want. First, the mcp[cli]==2.0.* pin, which conflicts with any environment holding mcp 1.x. Second, a package whose identity resolution raises outside the server request scope by design. So every in-process consumer that wants to honour the contract reimplements the token logic, and drifts from the behaviour the MCP server enforces.

Outcome

mint_confirmation_token, verify_confirmation_token, classify_confirmation_token_failure and confirmation_signing_key are importable from pipefy_sdk. An in-process consumer runs the same protocol the MCP server runs, from the same code.

  • The module is pipefy_sdk.destructive_confirmation, stdlib-only. The SDK dependency list is unchanged: 8 Requires-Dist entries, read off the built wheel.
  • The token wire format is unchanged and pinned by a golden vector, so a token minted by one build verifies on another during a rolling deploy.
  • confirmation_signing_key is new. It derives one caller's HMAC key from that caller's credential, which is what stops one caller's token from confirming another's deletion. signing_key_for in the MCP guard calls it, so both sides share one derivation.
  • pipefy_mcp.tools.destructive_confirmation_token is deleted. The guard imports from pipefy_sdk.
  • The MCP protocol is unchanged. No tool name, schema, annotation, envelope, or preview text moves, so hosted and remote callers cannot observe this.

Read packages/sdk/src/pipefy_sdk/destructive_confirmation.py first. Its only change against the old module is the added confirmation_signing_key. The three original functions and every private helper are byte-identical.

Testing

Offline: full suite 4762 passed / 39 skipped, ruff check, ruff format --check, lint-imports (2 contracts kept), bump_version.py verify, the skill-reference and Cursor-plugin linters, uv build --all-packages.

The live runs below drove the pipefy-mcp-server binary built from this branch, over the real MCP protocol, against a prod organization. They used throwaway pipes only. All seven were created and deleted by the runs, and each deletion was confirmed afterwards.

Local profile, stdio

# Scenario Expected Observed Verdict
0 Server built from this branch serves the tool surface delete_pipe registered 187 tools, present PASS
1 delete_pipe confirm=false Preview, token minted, no deletion requires_confirmation=true, token minted PASS
2 get_pipe after the preview Pipe still exists Returned the pipe PASS
3 confirm=true, token omitted Previews again, no deletion requires_confirmation=true PASS
4 Tampered token, MAC no longer matches Rejected, no deletion requires_confirmation=true PASS
5 Pipe B's token replayed against pipe A Identity mismatch, no deletion requires_confirmation=true PASS
6 get_pipe after three rejected confirmations Pipe still exists Returned the pipe PASS
7 confirm=true with the matching token Pipe is deleted Pipe ... was permanently deleted. PASS
8 get_pipe immediately after the deletion Not found Still resolved see below
9 Spent token for A replayed against B Identity mismatch, B survives requires_confirmation=true PASS

Row 8 is a property of the Pipefy API, not of this change. Pipe deletion is eventually consistent: polling shows the pipe stops resolving 6.6 s after the mutation returns success. The deletion mutation itself is untouched SDK code.

A separate stdio run replayed one token across two server processes. Process B rejected process A's token. That confirms the local profile falls back to the per-process random key and never reaches confirmation_signing_key.

Remote profile, Streamable HTTP with a validated bearer

# Scenario Expected Observed Verdict
0 RFC 9728 protected-resource metadata 200 naming this resource 200, resource matches PASS
1 tools/list with no bearer 401 401 PASS
2 Validated bearer reaches the remote-safe surface delete_pipe present, surface smaller than local's 187 184 tools, present PASS
3 delete_pipe confirm=false Preview, token minted, no deletion requires_confirmation=true, token minted PASS
4 Token minted by process A, confirmed against a fresh process B, same bearer Verifies and deletes Pipe ... was permanently deleted. PASS
5 Pipe A's token replayed against pipe B, same caller Identity mismatch, B survives requires_confirmation=true PASS

Row 4 covers the changed line. The same cross-process replay is rejected on stdio. A token survives a server restart only when its key comes from the caller rather than from the process, so row 4 is what proves confirmation_signing_key(bearer) runs.

Not verified: mcp.pipefy.com. The hosted deployment pins an exact published release of pipefy-mcp-server (0.5.0b1 today), so it cannot exercise this branch. Hosted coverage follows the release and the pin bump. The remote profile above therefore ran locally against the real issuer.

Notes

Issue #649 asked for a deprecated re-export shim at the old path. This drops it. pipefy_mcp.tools.__init__ exports only PIPEFY_TOOL_NAMES and ToolRegistry, and docs/DEPRECATION.md scopes the MCP surface to tool names and argument shapes, so the deleted path was never public.

The SDK ships ungated delete_*/remove_* methods and a docstring
obligation to gate them, but not the code that does the gating. The
canonical implementation lived in pipefy-mcp-server, which an in-process
SDK consumer cannot import without taking the mcp[cli]==2.0.* pin and a
package whose identity resolution raises outside the server request
scope. So each consumer reimplements the token logic and drifts from
what the MCP server enforces.

Move mint_confirmation_token, verify_confirmation_token and
classify_confirmation_token_failure into
pipefy_sdk.destructive_confirmation and export them from the package
root. The three functions and every private helper are byte-identical,
so the token wire format does not move; a golden vector pins it.

Add confirmation_signing_key, which derives one caller's HMAC key from
that caller's credential. Deriving per caller is what stops one caller's
token from confirming another's deletion, and the SDK previously shipped
that instruction with no helper behind it. signing_key_for in the MCP
guard now calls it, so both sides share one derivation.

Delete pipefy_mcp.tools.destructive_confirmation_token rather than leave
a deprecated shim. pipefy_mcp.tools exports only PIPEFY_TOOL_NAMES and
ToolRegistry, and docs/DEPRECATION.md scopes the MCP surface to tool
names and argument shapes, so the path was never public.

The module is stdlib-only and the SDK dependency list is unchanged. No
tool name, schema, annotation, envelope, or preview text changes.

Closes #649.

Signed-off-by: mocha06 <52426811+mocha06@users.noreply.github.com>
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.

1 participant