Skip to content

feat(cli): destructive-command tiers — confirm + interactive-only deletes - #119

Draft
joshdholtz wants to merge 6 commits into
mainfrom
dx-948-destructive-approval-evals
Draft

feat(cli): destructive-command tiers — confirm + interactive-only deletes#119
joshdholtz wants to merge 6 commits into
mainfrom
dx-948-destructive-approval-evals

Conversation

@joshdholtz

@joshdholtz joshdholtz commented Aug 17, 2026

Copy link
Copy Markdown
Member

Two tiers for destructive commands, with behavioral guards for both:

Destructive (products push, publish/unpublish, webhooks delete, refunds, subscriptions cancel, store discard, customer grant/revoke/transfer): refuse under --no-input without --yes; --yes lets them through. A fake API that only allows GET, so nothing mutates before consent.

Interactive-only (apps / paywalls / offerings / entitlements / products / packages delete): must be run by a person in a terminal. --json and --no-input are refused, and --yes can't bypass it. These are irreversible, so we don't want agents firing them — manual, deliberate use only.

A shared requireInteractive gate sits next to confirmOrAbort. The guard test has one list per tier; a new human-only command is a gate line plus a list entry. The paywall force-check was extracted so it stays unit-tested now that the command itself is interactive-only.

🤖 Generated with Claude Code


Note

Medium Risk
Changes consent and automation paths for destructive CLI commands, including a breaking restriction that scripts can no longer delete apps, paywalls, offerings, entitlements, products, or packages.

Overview
Splits destructive CLI actions into two consent tiers so irreversible catalog deletes cannot be fired by automation.

Interactive-only (apps / paywalls / offerings / entitlements / products / packages delete): new requireInteractive refuses --json, --no-input, and non-TTY sessions. --yes still skips the prompt only in a real terminal.

Confirm-with---yes: other mutating commands (push, publish/unpublish, webhooks delete, refunds, cancel, store discard, customer grant/revoke/transfer) still refuse --no-input without --yes.

Adds a guard test that drives the real commands against a GET-only fake API, and extracts checkPaywallDeletable so the --force rule stays unit-tested now that paywall delete is interactive-only.

Reviewed by Cursor Bugbot for commit 9fc277a. Bugbot is set up for automated code reviews on this repo. Configure here.

@linear-code

linear-code Bot commented Aug 17, 2026

Copy link
Copy Markdown
DX-948 Add evals verifying agents ask for human approval before destructive CLI operations

The CLI now gates destructive paywall operations behind --force + steering that tells the agent to ask a human first (PWAI-352). But steering is a request, not a guarantee — agents can ignore it, and behavior varies significantly across models. We have no automated check that the steering actually works.

Ask: build an eval suite that verifies agents pause for human approval before destructive operations (e.g. rc paywalls delete on a published/attached paywall, and any future destructive commands).

  • Run as a model matrix since behavior varies: Grok, Opus/Sonnet, 5.6 Sol/Terra/Luna, Kimi, Gemini.
  • Assert the agent surfaces a confirmation / stops and asks, rather than proceeding with --force on its own.
  • Regression-guard this so future steering/description changes don't silently weaken the safeguard.

Inspired by Railway's approach (they run evals to ensure agents ask for human approval before destructive actions).

Context / discussion: https://revenuecat.slack.com/archives/C0AG3QP8PFA/p1786455572433679?thread_ts=1786440167.594769&cid=C0AG3QP8PFA

Related: PWAI-352 (delete guard), WFL-386 (restore deleted paywalls), EXP-314 (protect experiment-linked paywalls).

Review in Linear

@joshdholtz

Copy link
Copy Markdown
Member Author

bugbot run

Comment thread internal/cli/destructive_guard_test.go
@joshdholtz

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0db4c8a. Configure here.

@joshdholtz

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1f25d1e. Configure here.

@joshdholtz joshdholtz changed the title test(cli): drift guard that destructive commands require confirmation test(cli): behavioral guard that destructive commands require confirmation Aug 17, 2026
@joshdholtz
joshdholtz force-pushed the dx-948-destructive-approval-evals branch from 1f25d1e to ac1d7fa Compare August 18, 2026 15:48
@joshdholtz
joshdholtz marked this pull request as ready for review August 19, 2026 03:21
Copilot AI lite review requested due to automatic review settings August 19, 2026 03:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@joshdholtz
joshdholtz marked this pull request as draft August 19, 2026 03:27
@joshdholtz
joshdholtz force-pushed the dx-948-destructive-approval-evals branch from ac1d7fa to 0f5c6dd Compare August 19, 2026 17:02
@joshdholtz joshdholtz changed the title test(cli): behavioral guard that destructive commands require confirmation feat(cli): destructive-command tiers — confirm + interactive-only deletes Aug 19, 2026
@joshdholtz

Copy link
Copy Markdown
Member Author

bugbot run

Comment thread internal/cli/confirm.go
@joshdholtz

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ec7953b. Configure here.

@joshdholtz
joshdholtz force-pushed the dx-948-destructive-approval-evals branch from 3fd6422 to ec7953b Compare August 20, 2026 17:07
joshdholtz and others added 6 commits August 20, 2026 13:10
… (DX-948)

Enumerate commands whose verb mutates or removes real state
(delete/revoke/refund/cancel/transfer/grant/push/publish/unpublish/apply/
discard/simulate-purchase) and assert each routes through confirmOrAbort so a
new unguarded destructive command fails CI. A static AST guard follows helper
delegation (e.g. products store apply -> applyStoreStatePlan); a behavioral test
drives real commands and proves each refuses under --no-input without --yes
before any state-changing call, and that --yes lets it through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uard

The verb list named grant and transfer but the behavioral test never drove
them, so a regression that confirmed after mutating would pass CI. Both now
run under --no-input and must hit the confirmation gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d check

The static AST enumeration looked thorough but had silent blind spots (variable
Use strings, verb-list gaps, defeatable by refactoring) — false confidence. The
behavioral test that drives each destructive command and asserts it refuses
under --no-input without --yes (and --yes bypasses) is the honest guard; new
destructive commands are covered by adding them to the driven list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apps/paywalls/offerings/entitlements delete now require a person at a real
terminal: --json, --no-input, and non-TTY are refused, and --yes can't bypass
it, so automation can't fire the deletes that are hardest to undo. Adds a shared
requireInteractive gate next to confirmOrAbort and extends the destructive-guard
test with a second tier: destructive commands still allow --yes, interactive-only
commands refuse even with it. Extracts the paywall force-check so it stays unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the interactive-only tier to products delete and packages delete — both
irreversible like the other four — so no destructive delete is agent-drivable.
Moves them out of the --yes-bypass list and into the interactive-only guard;
switches the yes-bypass test to webhooks delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CanPrompt is also false in a plain non-TTY session with no flags, but
the gate error only named --json/--no-input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joshdholtz
joshdholtz force-pushed the dx-948-destructive-approval-evals branch from 1e448bc to 9fc277a Compare August 20, 2026 18:10
@joshdholtz

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9fc277a. Configure here.

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