feat(execute): add --require-verified and --timeout to execute status - #95
feat(execute): add --require-verified and --timeout to execute status#95vaibhav4046 wants to merge 1 commit into
Conversation
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
left a comment
There was a problem hiding this comment.
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:
ExecStatusResponsegains aReceipts []ExecReceiptfield;renderExecStatusprints one table row per receipt (hash (receiptStatus, verified|unverified)).- New
--require-verifiedflag onkh ex status:renderExecStatusCheckedcallsverifyExecReceipts, which fails unlessStatus == "completed", at least one receipt exists, and every receipt hasVerified == trueandReceiptStatus == "success". - New
--timeoutflag (default 5m) on--watch.watchExecStatusnow computesdeadline := time.Now().Add(timeout)and checks it on both the ticker branch and the idle-sleep branch, returning a timeout error once exceeded. Previously--watchpolled with no deadline. status_verified_test.go(new, 246 lines): coversverifyExecReceipts's branches (no receipts, unverified, each non-successreceiptStatus), back-compat without the flag,--watch --require-verifiedreaching a verified terminal state, and--watch --timeout 100msagainst 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-verifiedis opt-in and purely additive - it does not need the--watchtimeout to exist or be correct. - The
--timeout/deadline addition to--watchis unrelated to receipt verification and changes default behavior for every existing--watchcaller (previously unbounded polling, now capped at 5m unless overridden) - it does not need receipts or--require-verifiedto 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 afterfetchExecStatusreturns or in the idle-sleep branch, and nothing in the request path (fetchExecStatus->client.Do->retryablehttp.Client.Doininternal/http/client.go) attaches acontext.WithTimeout/context.WithDeadlineor setshttp.Client.Timeout-> if the server accepts the connection but never responds (stalled connection, silent network partition),client.inner.Do(req)blocks insidecase <-ticker.C:indefinitely and the deadline check at line 208 is never reached ->kh ex st <id> --watch --timeout 5shangs forever despite the flag's own doc string promising "Give up watching after this long." -> fix: make the request itself bounded, e.g. threadcontext.WithTimeout(ctx, timeout)throughclient.NewRequest/client.Do. Note this same gap already exists inpollExecStatus(transfer.go,--wait --timeout), which this PR's description says it mirrors - see Needs a decision.
Mechanical - actionable as-is
cmd/execute/status.go:209vsstatus.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 --timeouttest only exercises a server that responds promptly withpendingon 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
watchExecStatusmirrors an identical, pre-existing gap inpollExecStatus(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 tointernal/http/client.go, touching code outside this PR's stated scope; (b) fix onlywatchExecStatushere and file thepollExecStatusgap separately; (c) leave both and document the limitation in both--timeoutflag descriptions.
Verdict
Changes requested - the --timeout flag does not reliably bound a hung request, and the PR bundles two independently shippable changes.
Problem
kh execute statustreatsstatus=completedas final proof of success. But a completed execution without chain-verified receipts only proves the transaction was submitted, not that it landed. Agents and CI scripts that gate on this command can proceed on unproven state.Separately,
kh execute status --watchhas no deadline: if an execution never reaches a terminal state, the command polls forever. (kh execute transfer --waitalready has a--timeoutfor exactly this reason;status --watchdoes not.)Related: #49 asks for better executionId -> status lookup ergonomics for agent workflows; this PR makes the existing lookup safe to gate on.
Changes
receipts[]fromGET /api/execute/{id}/status(hash,chainId,verified,receiptStatus,blockNumber,gasUsed,verifiedAt) and render each receipt in the status table.--require-verifiedflag: exit non-zero unless the execution iscompletedAND at least one receipt exists AND every receipt hasverified=truewithreceiptStatus="success". Fails closed onreverted,not_found,timeout, andsafe_inner_failure.--timeoutflag (default 5m) for--watch, mirroring the deadline pattern inpollExecStatusused bytransfer --wait.--require-verified, output and exit behavior are unchanged (receipts are shown when present).Why fail-closed
A
transactionHashproves submission; a verified receipt proves landing. For agent pipelines that chain onchain steps (kh ex st <id> --watch --require-verified && ./next-step.sh), the safe default for the strict flag is: no proof -> non-zero exit.Tests
7 new cases in
cmd/execute/status_verified_test.go(httptest fake server, same idiom as the existing status tests):--require-verified-> exit 0, receipt rendered--require-verified-> non-zero, "no receipts"verified=falsereceipt -> non-zero, offending hash in error--watch --require-verified-> polls pending -> completed+verified -> exit 0--watch --timeout 100msagainst a never-terminal execution -> non-zero with "timeout"go vetclean,gofmtclean,go test ./cmd/execute/green.Note: on Windows, 8 pre-existing agentic-wallet/doctor tests fail on a clean clone of main (appears to be a HOME vs USERPROFILE issue in test setup); untouched by this PR.