Skip to content

fix(cli): non-interactive stdin auto-confirms irreversible admin and vote commands - #1650

Open
RealDiligent wants to merge 1 commit into
entrius:testfrom
RealDiligent:fix/critical-issue-non-interactive-confirm-bypass
Open

fix(cli): non-interactive stdin auto-confirms irreversible admin and vote commands#1650
RealDiligent wants to merge 1 commit into
entrius:testfrom
RealDiligent:fix/critical-issue-non-interactive-confirm-bypass

Conversation

@RealDiligent

@RealDiligent RealDiligent commented Jul 24, 2026

Copy link
Copy Markdown

Summary

confirm_or_abort — the confirmation gate in front of every destructive issue-bounty command — returns True whenever stdin is not a TTY:

if yes or not _is_interactive():
    return True

So the safeguard silently disappears in exactly the environments where a mistake is least recoverable. These commands run unattended all the time: systemd, pm2, cron, docker run without -it, a shell pipeline, or a non-interactive ssh host 'gitt ...'.

Eight destructive call sites are affected:

Command Effect when auto-confirmed
admin set-owner Transfers contract ownership — the code itself warns "IRREVERSIBLE. A mistyped address makes the contract unrecoverable."
admin set-treasury Resets every Active/Registered issue's bounty amount to 0
admin payout-issue Irreversible ALPHA transfer
admin cancel-issue Returns a bounty to the alpha pool
admin add-vali / remove-vali Edits the validator consensus whitelist that authorises bounty payouts
vote solution / vote cancel Casts a consensus vote that moves bounty funds

issues register (which spends real ALPHA) carried an inline copy of the same expression.

This also made the documented opt-in a no-op — --yes is advertised as "Skip confirmation prompt (non-interactive/CI)", but non-interactive runs already skipped it, so passing the flag changed nothing.

Root cause

confirm_or_abort collapses two distinct cases into one branch: "the operator explicitly approved" (yes) and "nobody can be asked" (no TTY). The second is treated as approval — a fail-open default on irreversible operations.

Fix

Fail closed. --yes remains the only way to skip the prompt; a non-interactive stdin now exits 1, naming both the action and the flag to pass. This is the convention used by apt, docker, terraform, gh, and btcli. issues register now routes through the shared helper instead of duplicating the fail-open expression.

Scope is 2 source files, +8/−4 lines of logic.

CLI output evidence

Literal terminal captures of echo | gitt admin set-owner 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY (piped stdin ⇒ not a TTY, no --yes).

Before — the confirmation never happens. Execution runs straight past the gate into wallet/contract-client loading; in this capture environment it stops only because an optional dependency is absent. On an operator machine with dependencies installed, this call submits the ownership transfer:

Network: finney • Contract: 5FWNdk8YNtNc...ew3MrD
┌──────────────────────────────────── Transfer Ownership ─────────────────────────────────────┐
│ New Owner: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY                                 │
│ This transfer is IRREVERSIBLE. A mistyped address makes the contract unrecoverable.         │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

✗ Missing dependency — No module named 'async_substrate_interface'

[process exit code: 1]

After — execution stops at the gate and names the flag required to proceed:

Network: finney • Contract: 5FWNdk8YNtNc...ew3MrD
┌──────────────────────────────────── Transfer Ownership ─────────────────────────────────────┐
│ New Owner: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY                                 │
│ This transfer is IRREVERSIBLE. A mistyped address makes the contract unrecoverable.         │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

✗ Confirmation required: Transfer ownership to
5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY?

stdin is not interactive — re-run with --yes to confirm.

[process exit code: 1]

With --yes the behaviour is unchanged from before this PR (prompt skipped, command proceeds).

Related Issues

None open — this behaviour has not been reported previously.

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Other (describe below)

Testing

  • Tests added/updated
  • Manually tested

New tests/cli/test_confirm_or_abort.py (8 cases) covers the interactive accept/decline paths, both --yes paths, and the three fail-closed cases. The fail-closed tests were verified to fail against the unpatched helper and pass with it.

Full CI on this branch: 968 tests passed; ruff lint, ruff format, pyright, and vulture all pass with no auto-format changes.

One existing test (test_vote_solution_import_error_exits_non_zero) reached its import-error path only because non-TTY stdin self-confirmed — CliRunner stdin is never a TTY. It now passes --yes, preserving the test's original intent.

Risk / tradeoffs

Deliberately a behaviour change: a script that today relies on the implicit non-TTY skip will now exit 1 instead of proceeding. That is the point of the fix, and the error message states the one-flag remedy. Anyone already following the documented --yes pattern sees no change.

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Changes are documented (if applicable)

🤖 Generated with Claude Code

@xiao-xiao-mao xiao-xiao-mao Bot added the bug Something isn't working label Jul 24, 2026
…irmation

`confirm_or_abort` returned True whenever stdin was not a TTY, so every
destructive issue-bounty command self-confirmed when run unattended.

The affected commands transfer contract ownership (documented in-code as
"IRREVERSIBLE. A mistyped address makes the contract unrecoverable"), pay out
and cancel bounties, reset active bounty amounts, and add/remove hotkeys from
the validator consensus whitelist. Any of them run under systemd, pm2, cron,
`docker run` without -it, a shell pipeline, or a non-interactive ssh command
executed immediately with no confirmation — which also made the documented
`--yes` opt-in ("Skip confirmation prompt (non-interactive/CI)") a no-op.

Fail closed instead: `--yes` remains the only way to skip the prompt, and a
non-interactive stdin exits 1 naming the action and the flag to pass. This
matches the convention used by apt, docker, terraform, gh, and btcli.

`issues register`, which spends real ALPHA, carried an inline copy of the same
fail-open expression; it now routes through the shared helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant