feat: architecture hardening — agent contract, pricing truth, wizard engine, timeouts, MCP, verified updates - #57
Merged
Merged
Conversation
…tion - Dynamic prompter: Prompter()/Status() resolve agent mode at call time (the factory is built before flag parsing); agent mode returns a fail-fast agentPrompter (INTERACTIVE_PROMPT_BLOCKED) and never a spinner. - ErrCancelled + propagation sweep: wizard engine returns sentinel wizard.ErrCancelled; commands map prompt cancel (Ctrl+C/Esc) to clean nil exits while propagating real prompt/engine errors instead of swallowing them. Bubble Tea prompts fail fast with tui.ErrNoTerminal when stdin is not a TTY (pipe/redirect) instead of redrawing forever. - --yes gates: volume create/delete (single and --all), ssh-key delete, startup-script delete and template delete require --yes in agent mode (CONFIRMATION_REQUIRED); --yes skips confirmation interactively. - Structured outputs + tests: agent-mode JSON results for create/delete; regression tests for lazy prompter resolution, fail-fast prompts and the --yes gates; README updates for the new flags.
…ode guarantees)
Add tests/contract/, a Go test package with no build tags that builds the
real verda binary once (TestMain, go build into a temp dir) and drives it
against an in-process mock of the Verda API. No network, no real
credentials, parallel-safe (each test gets a fresh mock server).
Harness
- tests/contract/mockapi: per-test httptest server with an isolated fixture
store reusing the SDK wire types. Covers POST /oauth2/token (JSON + the
SDK's form-encoded 400 retry), GET /instance-types, GET/POST /instances,
PUT /instances (actions), GET/POST /volumes, GET/DELETE /volumes/{id},
GET/POST/DELETE ssh-keys, GET /scripts, /locations, /instance-availability,
/balance; unknown routes answer 404 JSON. FailRoute() overrides any exact
path with a status code; created instances report "provisioning" once then
"running" from their first read, so --wait pollers converge without sleeps.
- Run harness: env isolation via VERDA_HOME+tempdir, mock credentials via
VERDA_CLIENT_ID/VERDA_CLIENT_SECRET, --base-url flag per invocation, cwd
pinned to a temp dir (no config.yaml poisoning), stdin is /dev/null so any
prompt regression hangs loudly instead of passing, 30s per-command
deadline; TestMain warm-up run absorbs the macOS dyld/Gatekeeper
first-launch penalty (~60s on cold binary content) once instead of letting
parallel tests eat per-command timeouts.
Contracts covered
- --agent vm list -o json: stdout is pure JSON (valid parse, zero ANSI
bytes), exit 0, stderr empty.
- --agent settings theme with stdin /dev/null: INTERACTIVE_PROMPT_BLOCKED
with choices, exit 2, wall-clock < 5s (catches blocking regressions).
- --agent volume delete: CONFIRMATION_REQUIRED without --yes (mock state
untouched), structured delete result with --yes.
- Error classification per docs/agent-errors.md: 401 -> AUTH_ERROR exit 3,
500 -> API_ERROR exit 4, both with details.status.
- Table mode: instance table on stdout, nothing on stderr.
- C1 pricing contract: the 8-GPU catalog type is priced at exactly 8x its
1-GPU sibling and CPU.4V.16G carries the staging ground-truth totals
(0.0279 on-demand / 0.0098 spot); vm create/describe/list must pass
price_per_hour through as the wire TOTAL — a re-multiplication regression
breaks exact equality.
- --debug redaction: token exchange leaks neither client_secret nor the
issued token. The form-encoded fallback subtest reproduces review
H1 (verified failing: secret printed verbatim) and is skipped pending the
fix PR.
Wire-up: no build tag, so the suite runs under "make test"
(go test ./...) and CI's Test job unchanged; tests/integration (staging)
untouched; -short skips the binary build.
The --wait default was evaluated via f.AgentMode() at command-tree
construction, but the factory is built before flags are parsed (cmd.go),
so the default was always true: --agent vm create polled instance status
up to the 5m --wait-timeout instead of returning after issuance.
runCreate now computes the effective wait at runtime: agent mode returns
the issued instance immediately unless --wait was passed explicitly.
Tests:
- unit: agent-mode create with default --wait performs zero
GET /instances/{id} polls; explicit --wait polls (create_test.go)
- contract: TestAgentVMCreateReturnsAfterIssuance asserts the same at
the wire level via a new mockapi InstanceGetCount; mock flips
instances to running on first read so polling alone is not observable
Verified live on staging (temp/docs/c1-ondemand-instance.json, review C1): price_per_hour on both the instances and instance-types endpoints is the TOTAL hourly price of the instance. cmdutil.InstanceTotalHourlyCost multiplied it by GPU/vCPU count again, so the status dashboard overstated burn rate Nx (an 8-GPU instance reported 8x its real cost). - status: burn rate is the plain sum of instance price_per_hour totals; delete InstanceTotalHourlyCost/InstanceBillableUnits/ InstanceTypeBillableUnits - volume pricing: single source of truth in cmdutil (VolumeHourlyPrice/VolumeMonthlyPrice/HoursInMonth), replacing five divergent copies (wizard summary, vm create display, cost estimate, cost running, MCP estimate); MCP used a /30/24 month with per-GB ceiling, now the house formula - MCP estimate monthly horizon 24*30 -> 730 - MCP get_running_costs now aggregates attached-volume costs, mirroring cost running - cost estimate: unknown --storage-type fails listing valid types (was silent /bin/bash); same guard on volume create --type; drop the never-read --location flag (API prices currency-only) from the command and the MCP estimate_cost tool - tests: TOTAL-semantics fixtures (CPU.4V.16G=0.0279/0.0098 staging ground truth), exact-equality 8-GPU burn regression test, money-path tests assert production computeTotals instead of inline sums - docs: root CLAUDE.md and vm/volume CLAUDE.md now state TOTAL semantics (they contradicted each other, which is how the bug kept getting re-derived)
…lidation Wizard engine hardening batch (review 2026-08-09, PR-C): - Race (C3): MessageBus is now mutex-guarded. stepLoop writes (Broadcast / store-change) while the composite tea program reads (RenderAll) once per frame on its own goroutine; -race flagged it. make test now runs go test -race so it cannot regress (CI Test job inherits it via make test). - Rewind guard self-defeat (HIGH): transition() zeroed rewindCount on every reset, so the maxRewindsPerStep guard never fired and a loader that kept returning empty choices bounced the user back forever (only Ctrl+C escaped). The count now resets only on stateCompleted — the transition that actually made progress. Regression test drives a deterministically-empty loader through both rewind paths (rewindToDependency and the rewindOne fallback) and asserts the guard fires after exactly maxRewindsPerStep+1 loader calls. - Cancel sentinel contract: Run never returns bare ErrCancelled. Ctrl+C returns ErrCancelled wrapping tui.ErrInterrupted, Esc at the first step returns ErrCancelled wrapping context.Canceled, so cmdutil.IsPromptInterrupt/IsPromptBack/IsPromptCancel classify both. main.go now maps IsPromptCancel to a silent exit 0 (before the agent-error branch), which also covers loader-time cancels arriving as "step %q: context canceled". Real engine/loader failures still propagate. errors.Is matrix pinned in cmd/util. - Inline Validate: step.Validate is wired into the TextInput prompt model (renders "✗ err" inline, blocks submit, input preserved — textinput.go model support). Other prompt types have no model-level hook, so a failed Validate now prints "✗ err" above the re-drawn prompt instead of silently redrawing and losing the answer. - Remove WithExitConfirmation everywhere (house rule wins): option, exitConfirm/interruptCancel state and confirmExit machinery deleted from the engine, all 8 production call sites cleaned, two exit-confirm integration tests dropped (Ctrl+C-now-exits is covered by TestIntegration_CtrlC_Exits). The "Exit wizard?" dialog contradicted the repo's Ctrl+C-is-terminal rule. - Spinner/progress Ctrl+C (H/M): the spinner no longer re-raises SIGINT into its own process (nondeterministic: could kill the process post-teardown or be swallowed while the op ran on). Models mark interruption and quit cleanly (final message intact); handles expose Interrupted(). cmdutil.WithSpinner/RunWithSpinner take ctx-taking closures: the op runs on a derived context the watcher cancels on interrupt, so Ctrl+C aborts the guarded work without killing the process. Editor/pager: Ctrl+C now returns tui.ErrInterrupted instead of collapsing into Esc/context.Canceled, consistent with select/confirm/textinput. - Docs: dropped stale ./pkg/tui/examples/wizard-views reference (examples are not vendored), swept exit-confirmation mentions (serverless CLAUDE.md, .ai/skills/new-command.md, move_wizard.go). - Latent races exposed by the new -race gate, fixed at the root: pkg/version no longer registers --version into the process-global pflag.CommandLine (cobra's updateParentsPflags merges it into every executed tree and its lazy sort cache raced across parallel tests); resumeFakeAPI in objectstorage tests is now mutex-guarded against the upload worker pool.
review H2: http.Client.Timeout covers the entire body read, so it clamped every request to opts.Timeout (flag values >30s silently capped at the 30s construction-time default) and killed multi-GB transfers mid-body. - factory: build the shared client without Timeout; dial/TLS bounds stay on http.DefaultTransport (per cc: lazy client still caps whole-body reads, so drop it entirely rather than making it lazy). - precondition sweep: vm action GetByID and update --verify now bound their fetches with WithTimeout(cmd.Context(), Options().Timeout) -- they previously relied on the client cap (and verify used context.Background(), so Ctrl+C could not abort it). doctor audited: checkAPIReachable self-bounds; CheckVersion self-bounds (2s). - registry: push/copy transfers run on cmd.Context() (Ctrl+C); the bubbletea view's cancel now unwinds a WithCancel transfer ctx. Tags / Head / dry-run manifest reads keep the --timeout-bound ctx. - registry copy/delete overwrite confirmations and objectstorage rm/rb/ abort-uploads/bucket-picker prompts run on cmd.Context() -- prompt think-time is never timeout-bounded; post-prompt mutations re-bind a fresh ctx so the pause can't drain the delete budget. - objectstorage sync/mv: mirror cp's data-plane precedent (cmd.Context() for transfer bodies); enumeration re-bounds around the list calls. - contract suite: mockapi gains HangRoute (blocks until client cancel); TestTimeoutControlPlaneFailsFast pins the --timeout bound for control plane; TestTimeoutTransferNotClamped copies through a source registry whose blob pulls outlast --timeout 900ms and must still succeed.
…rage review H1 (verified live): the SDK retries /oauth2/token form-encoded when the API rejects the JSON attempt, and --debug printed that body verbatim -- client_secret=... in clear text exactly when users capture logs for a bug report. - redactSensitiveBody picks the redactor by Content-Type: application/x-www-form-urlencoded bodies redact the secret form keys (client_secret, access_token, refresh_token, id_token, password, token); everything else keeps the JSON redactor (harmless no-op on non-JSON bytes). - JSON key list extended with the remaining secret-bearing SDK/API fields: secret_access_key, service_account_key, value_or_reference_to_secret, jupyter_token. - unit tests cover both content types, escaped-quote values, spacing variants, and content-type dispatch; the contract subtest previously skipped for H1 (mock ForceFormTokenFallback) now runs and asserts the redacted marker is present, not merely that the secret is absent.
review H3: keychainAuth resolved authn.DefaultAuthKey (index.docker.io) for ANY source, so (a) private ghcr/ECR/GCR/ACR sources never received their docker-config credentials and (b) a Docker Hub login was presented to whatever source registry a copy pulled from -- scope-confusion secret leak (ggcr's basicTransport attaches the resolved basic credential to every in-host request, even without an auth challenge). keychainAuth now carries the parsed source host (already normalized to index.docker.io for Hub refs by ggcr's parser) and Resolve is keyed on it; a miss still falls back to anonymous so public pulls keep working. Both callers (flag path and copy wizard) pass the parsed srcRef through buildSourceAuth. Coverage (fails pre-fix, verified by temporarily reverting the Resolve argument): - TestCopy_DockerConfigResolvesCredsForSourceHost: a private gated source (Basic challenge, exact credential required) copies successfully with host-keyed creds; the keychain is only ever resolved with the source host. - TestCopy_DockerConfigNeverSendsHubCredsToForeignHost: with only a Hub entry in the keychain, an anonymous public copy succeeds and the recorded wire traffic never carries the Hub credential.
- server: lazy client init via sync.Once — mcp-go dispatches tool calls on a worker pool, so the check-then-set on Server.client raced on the first parallel batch (list_vms + get_balance). Repro TestLazyClientInitConcurrent is red under -race pre-fix (16 factory calls), green after; factory errors latch (fix credentials, restart server). - confirm gates mirroring the CLI agent contract: create_vm/create_volume (billing) and vm_action shutdown/force_shutdown/hibernate/delete (destructive) require confirm:true, else the tool fails with CONFIRMATION_REQUIRED in the agent-error JSON envelope, before any API call. - vm_action honesty: default reports status accepted (was: completed without ever looking); wait:true polls via cmdutil.PollInstanceStatus and reports completed only when the instance reaches the expected status; failed transitions and timeouts are tool errors. delete is never polled. - wait.go: PollInstanceStatus errors on instance error/not_found (was: done+nil, so vm ... --wait exited 0 on failure); PollVolumeStatus stops immediately on canceled/deleted/error instead of burning the full timeout. - strict argument validation: mcp-go performs no schema validation, so all handlers type-check args explicitly; wrong types and out-of-set enums fail with VALIDATION_ERROR; estimate_cost with an unknown storage_type errors listing catalog types instead of pricing it $0; a bare-string ssh_key_ids no longer fans out to all account keys; string "500" no longer coerces to the 50GB default. - docs: new cmd/mcp CLAUDE.md+README.md (tool reference incl. confirm contract and accepted/completed semantics), agent-errors.md MCP section, README MCP safety contract. - tests: race repro, confirm gates, wait semantics, table-driven strict-arg coverage against tests/contract/mockapi (gained a /volume-types route), wait.go failure-propagation tests.
- runUpdate now verifies the downloaded ARCHIVE bytes against the
release's verda_<VER>_SHA256SUMS (goreleaser checksum pipe output,
exact-name match via new findArchiveChecksum/verifyArchiveChecksum in
verify.go) before extracting or replacing anything. Hash mismatch,
sums download failure, or a missing sums asset all abort with the
binary untouched and the error names --skip-verify as the escape
hatch. Binary sums (verda_<VER>_binary_SHA256SUMS) stay reserved for
'update --verify' of an installed binary.
- new --skip-verify flag documents the deliberate bypass.
- agent/json mode: update outcome is now written via WriteStructured
as updateResult{version, previousVersion, path, updated,
checksumVerified} instead of plain text; human/table output unchanged.
- scripts/install.sh fetches the same SHA256SUMS and checks only the
selected asset line (awk exact field match) with sha256sum -c or
shasum -a 256 -c before installing; mismatch/fetch failure aborts with
VERDA_INSTALL_SKIP_VERIFY=1 named as the escape hatch. New
VERDA_INSTALL_BASE_URL override enables local testing.
- tests: update_test.go runs four fixture-server scenarios (verified
happy path, tampered archive aborts with old binary intact, sums
endpoint 500 aborts, --skip-verify proceeds incl. JSON contract);
verify_test.go gains findMatchingChecksum boundary edges,
findArchiveChecksum and verifyArchiveChecksum units.
- cosign bundle verification of the sums files is documented as the
next step in cmd/update/CLAUDE.md (sigstore dep footprint), not
implemented here.
…mantics
- H4: applyTemplate now fills ONLY flags the user did not pass (cobra
Flags().Changed is the authority, including aliases); the same rule gates
image/ssh-key/startup-script name resolution. README contract
'--from gpu-training --location FIN-03' now lands in FIN-03.
- H5: template-wizard location 'None (decide at deploy time)' uses the
locationDecideLater sentinel so the engine's Default substitution no longer
persists FIN-01; the Setter translates it to an unset location and the
saved template stays locationless.
- H6: the contract step filters GetInstancePeriods codes through
normalizeContract, so undeployable duration-shaped long-term periods are
never offered (they failed at deploy time).
- H7: single interactive delete always passes an explicit empty volume_ids
when nothing is selected (nil invoked the API default of deleting the OS
volume, contradicting the keep-billing warning). Agent single delete
mirrors the batch contract: --yes required, volumes deleted only with
--with-volumes, batch-shaped JSON output.
- cc: template hostname patterns keep {location} for re-expansion against
the effective deploy location picked in the wizard.
- cc/batch-6: agent-mode vm action reports status accepted by default and
polls via cmdutil.PollInstanceStatus only on explicit --wait (failed
transitions become errors), matching the MCP vm_action contract.
When ls-uploads adopts a server-side multipart upload (no local checkpoint), the only guard was that the file covered the pre-tail bytes: a 4 MiB file adopting a 1-part 5 MiB upload would complete a 5 MiB object from the other file's bytes — silent corruption (review H9). When the chosen source file's implied part map is fully covered by the server-side parts, the tail part size must equal fileSize-(maxPart-1)*partSize; a mismatch refuses the adoption with a clear error before any mutation. Part sizes are the only available signal: S3 ListParts exposes multipart ETags, not content hashes. cp/mv are unaffected — they resume via local checkpoints already pinned to file size+mtime and never adopt checkpointless uploads (grep-verified: only resumeServerUpload calls listAllParts outside a checkpointed resume).
The 'explicitly set' check snapshotted the flag-bound fields before viper/env resolution, so an explicitly selected profile (--auth.profile / VERDA_PROFILE) overwrote env-supplied VERDA_CLIENT_ID/SECRET (and VERDA_AUTH_* via viper's AutomaticEnv binding) with the stored profile's values — silently targeting the wrong account in CI, contrary to the documented 'flags/env always win' contract. The profile now only fills fields still unset after flag > config/viper > env resolution, uniformly for auto-resolved and explicitly selected profiles. An explicit profile keeps its remaining jobs: choosing the section, pinning its verda_base_url, and surfacing profile-load errors. Matrix tests cover flag > config > env > credentials-file on both profile selection styles plus the VERDA_AUTH_* spelling. auth/README.md and docs/commands.md now describe the corrected precedence (the old 'explicit profile overrides env' example was the bug-as-documentation).
Stream wiring (review MEDIUM 'all standalone prompts render to os.Stdout'): - The factory built its prompter with tui.Default() and no IO, so a command that prompted while stdout was piped sprayed ANSI into the pipe and showed nothing to the user. NewFactory now takes the IOStreams and the registered Default/DefaultStatus builders honor their ioOpts (previously ignored). - New bubbletea stream split: prompts/spinner/progress/pager scroller render on ErrOut; Table and pager print-through (data) stay on Out, per the house rule. Spinner/progress go silent when the output is not a terminal instead of writing frames into pipes/capture buffers. - agentPrompter behavior (INTERACTIVE_PROMPT_BLOCKED fail-fast) unchanged. Agent error contract (docs/agent-errors.md promised exit 2 for bad input): - UsageErrorf returns a *UsageError; ClassifyError maps it to VALIDATION_ERROR with exit 2, dropping the human --help hint from the envelope. Converted the flag-misuse sites: volume delete --status/--all combinations, vm action --status/--hostname filters and --with-volumes on non-delete, batch --all combinations. Non-agent UX keeps the same message (+ help hint), exit 1. - Contract suite extended: interactive path with piped stdin fails fast with a clean stderr error and byte-empty stdout; agent usage errors assert exit 2 with the VALIDATION_ERROR envelope.
- tests: options and auth test helpers now use t.TempDir() instead of
os.MkdirTemp(".") — the worktree had accumulated 296 stale gitignored
tmp-test-* dirs (removed with this change).
- root CLAUDE.md: correct the three 'make test includes lint' claims (reality:
make test = go test -race; make lint is separate; pre-commit runs both) and
add the missing per-command doc rows (objectstorage, serverless, doctor) to
the index table. AGENTS.md's done-checklist line said the same — fixed.
- root README.md: 'verda update --version v1.0.0' -> '--target v1.0.0'
(--version is the print-version flag).
- CI pins: aquasecurity/trivy-action@master -> v0.36.0 (latest release),
pip install git-cliff -> ==2.13.1 (changelog + release workflows),
goimports@latest -> golang.org/x/tools v0.48.0 (ci.yml).
- goimports local-prefix typo (github/verda-cloud/verda-cli, missing .com):
deliberately deferred — fixing it regroups imports in ~100 files; one-line
deferral comment in .golangci.yaml and Makefile instead.
CI's GoSec lane runs gosec with --no-config (test files included) and flagged the new test fixtures/harness; CI lint (pinned golangci-lint v2.5.0) caught a goconst the local v2.11 didn't: - #nosec annotations with justification on fixture reads/writes (update_test, wizard_template_test) and the contract TestMain subprocess launches (binary path is harness-built under t.TempDir) - create.go validateKind: reuse kindCPU/kindGPU constants
CI pins golangci-lint v2.5.0; its gosec reports G204 at the launch statement (CombinedOutput/Run), not only at exec construction like the local v2.11 toolchain. Annotate both generations' sites.
… 1.1.0 - agent wait semantics documented: create/action return accepted after issuance; --wait polls to completed (with failure surfacing) - ssh-key delete, startup-script delete, volume create rows now show the --yes agent requirement (were: 'confirm first' / ungated docs) - add missing volume delete row (direct command, --yes gated) - manifest version 1.0.0 -> 1.1.0 so 'verda skills status' flags update_available to previously-installed agents
All three follow the Agent Skills directory convention (<name>/SKILL.md under a per-agent global dir), matching our existing verda-cloud + verda-reference frontmatter (OpenCode-validated: lowercase-hyphen name matching the directory): - kimi-code -> ~/.kimi-code/skills/ (also honored at ) - opencode -> ~/.config/opencode/skills/ - pi -> ~/.pi/agent/skills/ Manifest 1.1.0 -> 1.2.0 so skills status flags the update. Verified live: 'verda skills install kimi-code --force' installs v1.2.0 and both SKILL.md files land with frontmatter intact.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One feature branch, one commit per fix group (15 commits, +8.2k/−1.3k across 163 files). Originates from a full-repo adversarial review + second-opinion pass; full evidence write-up lives in
temp/docs/review-2026-08-09.md(gitignored working copy) — happy to move it intodocs/if we want it tracked.Every fix below was verified with a failing test first (or a live repro), then fixed, then gated by
make build / lint / test (-race).Test infrastructure (new)
tests/contract/— hermetic black-box suite: builds the real binary, drives it against an in-process mock Verda API (per-test fixture store, wire-path-faithful). Covers: agent-mode JSON purity (no ANSI on stdout),INTERACTIVE_PROMPT_BLOCKEDexit 2 (<5s, hang-proof), CONFIRMATION_REQUIRED gates actually blocking deletes, error classification (401→exit 3, 500→exit 4), stdout/stderr separation, C1 pricing end-to-end, debug redaction (incl. form-encoded OAuth fallback), transfer-timeout regression pins. Runs in defaultmake test; staging-gatedtests/integration/untouched.make testnow runs with-race(CI inherits).Commits (chronological)
2e21055fix(agent-mode)--agentcould hang on a real TUI). Now resolved lazily + full swallow sweep (nilerrsites propagating real errors) +--yesgates added where missing (volume create/delete, ssh-key delete) + structured success output + non-TTY (ErrNoTerminal) clean errors2ecc63atest(contract)16679dbfix(vm)--agent vm createhung ≤5min:--waitdefault was evaluated pre-flag-parse1a0ce4cfix(pricing)price_per_houris a TOTAL (verified live on staging, on-demand discriminator + fixturec1-ondemand-instance.json): status dashboard no longer multiplies by GPU/vCPU count (was 8× overstatement); one canonical volume-price helper (730h) replaces 5 divergent copies; unknown storage types now error instead of $0; dead--locationremoved; money-path tests assert production helpers instead of re-implementing sums45e0f54fix(wizard)go test -racered→green); rewind-guard self-defeat (infinite backward loop possible); cancel sentinel contract (ErrCancelled⊃ ErrInterrupted/context.Canceled, silent exit 0 on cancel, quiet main.go); step Validate renders inline instead of silently redrawing; exit-confirmation dialogs removed from all 8 wizards per house rule; spinner/progress/editor/pager Ctrl+C contract aligned (no self-SIGINT; ops abort via ctx); two latent races exposed by-racefixed at root (pkg/version pflag init, resume test fake)e224c5ffix(http)--timeout 120ssilently clamped); data-plane transfers (registry push/copy, objectstorage sync/mv) no longer die at 30s; interactive confirmations off the bounded ctx (6 sites); bare-context API calls swept (vm action,update --verify)7b00260fix(debug)--debugredaction now content-type aware — form-encoded OAuth fallback no longer leaksclient_secret; JSON key list covers secret_access_key/service_account_key/jupyter_token etc; contract test pins positive + negative assertionscd7e9ccfix(registry)3fb5f68fix(mcp)confirm: true(create_vm, create_volume, vm_action shutdown/force_shutdown/hibernate/delete);vm_actionis honest now (acceptedby default,wait:trueactually polls, failed transitions are errors); zero schema validation in mcp-go worked around with strict handler-side arg checks (no more "500"→0→50GB default, string ssh_key_ids→all-keys); estimate validates storage_type; vol/instance pollers treat error/not_found as failures6db023cfix(update)--skip-verifyescape hatch, named in the error);install.shverifies too (VERDA_INSTALL_SKIP_VERIFY=1/VERDA_INSTALL_BASE_URLfor tests); agent-mode update emits structured JSON; manual six-scenario installer verification recorded in commitf0cb18efix(vm)--from t --location Xfinally works as documented); template wizard "decide at deploy time" location no longer silently persists FIN-01; contract step filters undeployable periods (was: 13 steps then guaranteed failure); delete-volume semantics unified three ways (interactive explicit-empty / agent mirrors batch--with-volumes/ no more silent OS-volume deletion contradiction);{location}hostname expands with the effective location; agentvm actionhonors--waithonestly29cb468fix(objectstorage)c366554fix(options)81b85edfix(cli)86d656achoret.TempDir(); doc drift (make-test-doesn't-lint claims, README--target, doc index rows); CI pins (trivy-action@master→v0.36.0, git-cliff==2.13.1, goimports v0.48.0)Behavior changes reviewers should sign off on
--agent vm create/actionreturn after issuance by default;--waitopts back in.verda updatefails closed without network-reachable checksums (--skip-verifyto bypass).create_vm/create_volume/vm_actionrequireconfirm:true— MCP client configs/prompts may need a nudge.cost estimate --locationremoved (never implemented; pricing is currency-scoped).Deferred (recorded in commit messages / review doc)
Test plan
make build✓,make lint0 issues ✓make test(-race) green, multiple consecutive full runs; 31 packages incl. contract suite