Skip to content

feat(execute): require chain-verified receipts in status - #95

Open
vaibhav4046 wants to merge 2 commits into
KeeperHub:mainfrom
vaibhav4046:feat/execute-status-require-verified
Open

feat(execute): require chain-verified receipts in status#95
vaibhav4046 wants to merge 2 commits into
KeeperHub:mainfrom
vaibhav4046:feat/execute-status-require-verified

Conversation

@vaibhav4046

@vaibhav4046 vaibhav4046 commented Aug 9, 2026

Copy link
Copy Markdown

Problem

kh execute status currently treats status=completed as final proof of success. A completed execution without chain-verified receipts only proves that work reached a terminal API state; it does not prove that the transaction landed successfully. Agents and CI scripts need a fail-closed way to gate their next step on a verified receipt.

Related: #49 asks for better execution ID to status lookup ergonomics for agent workflows. This PR makes the existing lookup safe to use as a proof gate.

Changes

  • Parse receipts[] from GET /api/execute/{id}/status (hash, chainId, verified, receiptStatus, blockNumber, gasUsed, verifiedAt) and render each receipt in the status table.
  • Add --require-verified: return non-zero unless the execution is completed, at least one receipt exists, and every receipt has verified=true with receiptStatus="success".
  • Fail closed on reverted, not_found, timeout, and safe_inner_failure receipt states.
  • Preserve existing output and exit behavior when --require-verified is absent; receipts are simply shown when present.

Why fail closed

A transaction hash proves submission; a verified receipt proves landing. Agent pipelines can now safely gate subsequent steps:

kh execute status <id> --watch --require-verified && ./next-step.sh

No receipt or an unverified/failed receipt produces a non-zero exit.

Review scope correction

The earlier revision also added --timeout. Per review, that independent feature has been removed completely from this PR so receipt verification can ship on its own. A future timeout change can then use request-scoped cancellation and stalled-handler coverage rather than a post-response deadline check.

Tests

Six focused cases in cmd/execute/status_verified_test.go cover:

  1. completed + verified success receipt + --require-verified -> exit 0 and render receipt;
  2. completed + no receipts + --require-verified -> non-zero;
  3. completed + verified=false -> non-zero with the offending hash;
  4. terminal failure receipt states -> non-zero (table-driven subtests);
  5. completed + no receipts without the flag -> unchanged exit 0;
  6. --watch --require-verified -> pending then completed+verified -> exit 0.

Validation: gofmt clean and go test ./cmd/execute passes. go test ./... reaches unrelated pre-existing Windows agentic-wallet/doctor failures caused by the checkout's user-config path; this PR does not touch those packages.

kh execute status previously treated status=completed as final proof,
but a completed execution without chain-verified receipts only proves
submission, not landing. It also looped forever under --watch because
the poll loop had no deadline (unlike transfer's pollExecStatus).

- Parse receipts[] from the status response (hash, chainId, verified,
  receiptStatus, blockNumber, gasUsed, verifiedAt) and render them in
  the status table.
- Add --require-verified: exit non-zero unless the execution completed
  AND at least one receipt exists AND every receipt has verified=true
  with receiptStatus "success". Fails closed on reverted, not_found,
  timeout and safe_inner_failure.
- Add --timeout (default 5m) to --watch, mirroring the deadline
  pattern already used by kh execute transfer --wait.
- Back-compat: without --require-verified, behavior is unchanged.

Tests: 7 new cases in status_verified_test.go covering verified pass,
no-receipts fail, unverified fail, each non-success receiptStatus,
back-compat, watch + verify, and watch timeout.

@suisuss suisuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This repo has no CONTRIBUTING.md; the conventions that apply are enforced by CI and README - conventional-commit PR titles, go vet/gofmt clean, and tests alongside the changed command.

What this changes

cmd/execute/status.go:

  • ExecStatusResponse gains a Receipts []ExecReceipt field; renderExecStatus prints one table row per receipt (hash (receiptStatus, verified|unverified)).
  • New --require-verified flag on kh ex status: renderExecStatusChecked calls verifyExecReceipts, which fails unless Status == "completed", at least one receipt exists, and every receipt has Verified == true and ReceiptStatus == "success".
  • New --timeout flag (default 5m) on --watch. watchExecStatus now computes deadline := time.Now().Add(timeout) and checks it on both the ticker branch and the idle-sleep branch, returning a timeout error once exceeded. Previously --watch polled with no deadline.
  • status_verified_test.go (new, 246 lines): covers verifyExecReceipts's branches (no receipts, unverified, each non-success receiptStatus), back-compat without the flag, --watch --require-verified reaching a verified terminal state, and --watch --timeout 100ms against a permanently-pending execution.

Does it match the description

Scope creep - two independently shippable changes are bundled under one PR, and the description itself names them as separate ("Separately, kh execute status --watch has no deadline..."). Split test:

  • Receipts parsing/rendering + --require-verified is opt-in and purely additive - it does not need the --watch timeout to exist or be correct.
  • The --timeout/deadline addition to --watch is unrelated to receipt verification and changes default behavior for every existing --watch caller (previously unbounded polling, now capped at 5m unless overridden) - it does not need receipts or --require-verified to exist or be correct.

Each side deploys independently: status.go's receipt struct/rendering/--require-verified path could ship without touching watchExecStatus's deadline logic, and the deadline logic could ship without the Receipts field existing at all. Recommend splitting into (a) receipts + --require-verified and (b) --watch --timeout, each with its own subset of status_verified_test.go.

Blocking

  • cmd/execute/status.go:184-214 (watchExecStatus) - the deadline check only runs after fetchExecStatus returns or in the idle-sleep branch, and nothing in the request path (fetchExecStatus -> client.Do -> retryablehttp.Client.Do in internal/http/client.go) attaches a context.WithTimeout/context.WithDeadline or sets http.Client.Timeout -> if the server accepts the connection but never responds (stalled connection, silent network partition), client.inner.Do(req) blocks inside case <-ticker.C: indefinitely and the deadline check at line 208 is never reached -> kh ex st <id> --watch --timeout 5s hangs forever despite the flag's own doc string promising "Give up watching after this long." -> fix: make the request itself bounded, e.g. thread context.WithTimeout(ctx, timeout) through client.NewRequest/client.Do. Note this same gap already exists in pollExecStatus (transfer.go, --wait --timeout), which this PR's description says it mirrors - see Needs a decision.

Mechanical - actionable as-is

  • cmd/execute/status.go:209 vs status.go:213 - the ticker-branch timeout error is "timeout after %s: execution %s still %s" (includes the actual status) while the idle-branch timeout error is "execution %s still not terminal" (no status, different wording). Both fire at the same logical point (deadline exceeded mid-poll); use one consistent message.
  • status_verified_test.go's --watch --timeout test only exercises a server that responds promptly with pending on every poll - it does not cover a stalled/non-responding request, so it does not catch the Blocking item above. Add a case using a handler that blocks past the timeout duration.

Needs a decision

  • The hung-request gap in watchExecStatus mirrors an identical, pre-existing gap in pollExecStatus (transfer.go, --wait --timeout) - fixing it only in the new code duplicates the defect rather than removing it. (a) Fix both call sites now by adding context-aware requests to internal/http/client.go, touching code outside this PR's stated scope; (b) fix only watchExecStatus here and file the pollExecStatus gap separately; (c) leave both and document the limitation in both --timeout flag descriptions.

Verdict

Changes requested - the --timeout flag does not reliably bound a hung request, and the PR bundles two independently shippable changes.

@suisuss suisuss added changes-requested Triage: reviewed, changes needed from the contributor decision-needed Blocked on a maintainer decision, not on the contributor labels Aug 9, 2026
@vaibhav4046 vaibhav4046 changed the title feat(execute): add --require-verified and --timeout to execute status feat(execute): require chain-verified receipts in status Aug 11, 2026
@vaibhav4046

Copy link
Copy Markdown
Author

Updated per review: this PR now contains only receipt rendering and the fail-closed --require-verified\ gate. The timeout flag, implementation, and test were removed so timeout/cancellation can ship independently with request-scoped handling. \gofmt\ is clean and \go test ./cmd/execute\ passes. Ready for re-review.

@vaibhav4046

Copy link
Copy Markdown
Author

Review follow-up at 4e710be: the independent --timeout flag, deadline code, and timeout test are removed; this PR now contains only receipt rendering plus the opt-in fail-closed --require-verified gate. Local evidence: git diff --check, go test ./cmd/execute, and go vet ./... pass. go test ./... passes the changed package; remaining Windows user-config-path failures are in untouched cmd/doctor and internal/agentic. No unresolved inline review threads remain. Re-requesting @suisuss is permission-blocked (403), and CI run 31446213077 is action_required with zero jobs. Maintainer action needed: approve/run CI, then re-review this focused head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes-requested Triage: reviewed, changes needed from the contributor decision-needed Blocked on a maintainer decision, not on the contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants