Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2e21055
fix(agent-mode): prompt safety net, --yes gates, prompt-error propaga…
hi-lei Aug 9, 2026
2ecc63a
test(contract): hermetic black-box contract suite (mock API + agent-m…
hi-lei Aug 9, 2026
16679db
fix(vm): agent-mode create must not hang on default --wait
hi-lei Aug 9, 2026
1a0ce4c
fix(pricing): price_per_hour is a total; restore correct burn math
hi-lei Aug 9, 2026
45e0f54
fix(wizard): race-free bus, rewind guard, cancel sentinels, inline va…
hi-lei Aug 9, 2026
e224c5f
fix(http): drop client-level timeout; scope contexts to control plane
hi-lei Aug 9, 2026
7b00260
fix(debug): redact form-encoded OAuth fallback + full secret key cove…
hi-lei Aug 9, 2026
cd7e9cc
fix(registry): resolve source-registry credentials per host
hi-lei Aug 9, 2026
3fb5f68
fix(mcp): init race, confirm gates, honest action semantics, strict args
hi-lei Aug 9, 2026
6db023c
fix(update): verify checksums fail-closed on self-update and installer
hi-lei Aug 9, 2026
f0cb18e
fix(vm): template/flags precedence, truthful wizard, delete-volume se…
hi-lei Aug 9, 2026
29cb468
fix(objectstorage): resume adoption pins size of covered bytes
hi-lei Aug 9, 2026
c366554
fix(options): env credentials must beat stored profile
hi-lei Aug 9, 2026
81b85ed
fix(cli): prompts honor IO streams; agent validation errors exit 2
hi-lei Aug 9, 2026
86d656a
chore: doc drift, test tempdirs, CI pins
hi-lei Aug 9, 2026
a091cf8
chore: green up CI security/lint lanes
hi-lei Aug 9, 2026
228f689
chore: annotate remaining G204 launch sites for CI's pinned gosec
hi-lei Aug 9, 2026
1df3286
docs(skills): sync runbooks with hardening behavior changes; manifest…
hi-lei Aug 9, 2026
87c0dbf
feat(skills): install targets for Kimi Code, OpenCode, Pi
hi-lei Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ai/skills/new-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Shape of a flow:

```go
engine := wizard.NewEngine(f.Prompter(), f.Status(),
wizard.WithOutput(ioStreams.ErrOut), wizard.WithExitConfirmation())
wizard.WithOutput(ioStreams.ErrOut))
if err := engine.Run(ctx, flow); err != nil {
return err // Ctrl+C returns an error here — propagate it, like configure/vm/mv
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
fetch-depth: 0

- name: Install git-cliff
run: pip install git-cliff
run: pip install "git-cliff==2.13.1"

- name: Regenerate unreleased changelog
run: make changelog.unreleased
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH

- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports@latest
run: go install golang.org/x/tools/cmd/goimports@v0.48.0

- name: Check formatting
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
cache: true

- name: Install git-cliff
run: pip install git-cliff
run: pip install "git-cliff==2.13.1"

- name: Install goreleaser
uses: goreleaser/goreleaser-action@v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
uses: actions/checkout@v6

- name: Run trivy filesystem scan
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: "fs"
scan-ref: "."
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ verda

# Local smoke-test output (see test/run.sh)
tmp/
temp/

# Created by https://www.toptal.com/developers/gitignore/api/vim,jetbrains,vscode,git,go,tags,backup,test,emacs
# Edit at https://www.toptal.com/developers/gitignore?templates=vim,jetbrains,vscode,git,go,tags,backup,test,emacs
Expand Down
3 changes: 3 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,7 @@ formatters:
settings:
goimports:
local-prefixes:
# Known typo (missing .com): local grouping never matched, and fixing
# the prefix would regroup imports in ~100 files — deliberately
# deferred until a dedicated import-regroup change. Review LOW-2026-08-09.
- github/verda-cloud/verda-cli
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Skipping these steps leads to pattern violations, broken dual-mode, and pricing

- [ ] `make build` passes
- [ ] `make lint` passes with zero issues (do not rely on pre-commit to surface these)
- [ ] `make test` passes (runs lint + unit tests)
- [ ] `make test` passes (unit tests with -race; lint is the separate `make lint` item above)
- [ ] `--help` renders correctly for changed commands
- [ ] Interactive and non-interactive modes both work
- [ ] Interactive Selects pass `tui.WithShowHints(true)` so the hint bar renders
Expand Down
29 changes: 17 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ Go CLI for Verda Cloud. Cobra commands + Bubble Tea TUI + lipgloss styling.

```bash
make build # Build binary to ./bin/verda
make test # Run all tests (go test + golangci-lint)
make lint # Lint only
make test # Run all tests (go test -race)
make lint # Lint only (golangci-lint); also run by pre-commit hooks
make pre-commit # Full pre-commit suite
```

Never use raw `go test ./...` — always `make test` which includes linting.
Never use raw `go test ./...` — always `make test` (go test -race). Lint is separate: `make lint`; the pre-commit hooks run both.

## Architecture

Expand Down Expand Up @@ -43,14 +43,17 @@ Each command directory has its own `CLAUDE.md` (domain knowledge) and `README.md
| `cmd/registry/` | CLAUDE.md, README.md | Container registry (vccr.io): configure, configure-docker (alias login), show, ls, tags, push, copy, delete — beta (enabled by default, marked `(beta)` in `verda --help`) |
| `cmd/update/` | CLAUDE.md, README.md | CLI self-update |
| `cmd/settings/` | CLAUDE.md, README.md | CLI settings management |
| `cmd/objectstorage/` | CLAUDE.md, README.md | S3-style object storage: configure, mb/rb, cp/mv/sync/ls/rm, uploads, presign |
| `cmd/serverless/` | CLAUDE.md, README.md | Serverless containers and batch jobs |
| `cmd/doctor/` | — | Environment diagnostics |
| `cmd/availability/` | — | Instance availability by location |
| `cmd/cost/` | — | Balance, running costs, estimates |
| `cmd/images/` | — | OS image listing |
| `cmd/instancetypes/` | — | Instance type catalog |
| `cmd/locations/` | — | Datacenter locations |
| `cmd/status/` | — | Status dashboard |
| `cmd/ssh/` | — | SSH into instances |
| `cmd/mcp/` | | MCP server |
| `cmd/mcp/` | CLAUDE.md, README.md | MCP server (AI-agent tool surface; confirm gates, accepted/completed semantics) |
| `cmd/skills/` | — | AI skills management |
| `cmd/completion/` | — | Shell completions |

Expand All @@ -73,7 +76,7 @@ Each command directory has its own `CLAUDE.md` (domain knowledge) and `README.md

### Go House Style — avoid avoidable lint hits

The repo lints with `golangci-lint` via `make lint` (included in `make test`). These are the patterns the linters enforce — write them correctly the first time instead of fixing them in a second pass:
The repo lints with `golangci-lint` via `make lint` (also enforced by the pre-commit hooks, not by `make test`). These are the patterns the linters enforce — write them correctly the first time instead of fixing them in a second pass:

- **HTTP bodies** — use `http.NoBody` for GET/DELETE/etc., never `nil`. Close with `defer func() { _ = resp.Body.Close() }()`, not bare `defer resp.Body.Close()` (errcheck).
- **American English** — `behavior`, `canceled`, `artifact`, `checkered`, `gray`. `misspell` runs with `locale: US` and rejects British spellings in code and comments.
Expand All @@ -98,7 +101,7 @@ The repo lints with `golangci-lint` via `make lint` (included in `make test`). T

### Every API-calling command MUST:

1. **Timeout context**: `ctx, cancel := context.WithTimeout(cmd.Context(), f.Options().Timeout)`
1. **Timeout context**: `ctx, cancel := context.WithTimeout(cmd.Context(), f.Options().Timeout)` for control-plane calls. Data-plane transfers (registry push/copy, object-storage cp/mv/sync) run on `cmd.Context()` — Ctrl+C is the stop signal; a multi-GB transfer legitimately outlives `--timeout`. Interactive prompts also get `cmd.Context()`, and work resumed after a prompt re-bounds its API ctx so prompt think-time can't drain the budget. The shared `http.Client` carries NO `Timeout` — the client cap covers whole-body reads and would clamp transfers.
2. **Spinner**: Show spinner during API calls, stop before handling result
3. **Debug output**: `cmdutil.DebugJSON(ioStreams.ErrOut, f.Debug(), "label:", data)`
4. **Dual mode**: Work with flags (non-interactive) AND prompts (interactive) — no partial wizard
Expand All @@ -118,10 +121,11 @@ The repo lints with `golangci-lint` via `make lint` (included in `make test`). T

### Pricing — get this wrong and users get billed wrong:

- Instance `price_per_hour` from API is **per-unit** (per-GPU or per-vCPU)
- Total = `price_per_hour * units` — use `cmdutil.InstanceTotalHourlyCost(inst)`
- Volume: `price_per_month_per_gb` — hourly = `ceil(monthly * size / 730 * 10000) / 10000`
- Never display raw API price as "total" without multiplying
- Instance `price_per_hour` from the API (instances AND instance-types endpoints) is the **TOTAL** hourly price of the instance. Never multiply by GPU/vCPU count. Verified live on staging 2026-08-09 (`temp/docs/c1-ondemand-instance.json`; review C1).
- Burn rate = plain sum of instance `price_per_hour` totals (+ volume `base_hourly_cost`).
- A per-unit price shown to the user is total **divided** by units (GPU count or vCPU count) — division only, and only for display.
- Volume hourly: `cmdutil.VolumeHourlyPrice(monthlyPerGB, sizeGB)` = `ceil(monthlyPerGB * sizeGB / HoursInMonth * 10000) / 10000` — the only sanctioned formula (MCP and all CLI surfaces use it).
- Volume monthly: `cmdutil.VolumeMonthlyPrice(monthlyPerGB, sizeGB)`; hourly→monthly estimates use `cmdutil.HoursInMonth` (730 = 365*24/12, matching the web frontend).

### Credentials

Expand Down Expand Up @@ -157,10 +161,11 @@ Before considering any change complete:

```bash
make build # Must compile
make test # Must pass (tests + lint)
make test # Must pass (go test -race)
make lint # Must pass (golangci-lint; also run by pre-commit hooks)
```

`make test` runs `golangci-lint` — **never** report work as complete with lint failures outstanding. Fix them before the "done" message; don't defer to the pre-commit hook. See the "Go House Style" section above for the patterns that prevent the common hits.
**Never** report work as complete with lint failures outstanding. Fix them before the "done" message; don't defer to the pre-commit hook. See the "Go House Style" section above for the patterns that prevent the common hits.

If you modified a command, also verify:
- `./bin/verda <command> --help` renders correctly
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ lint.fix: ## Run golangci-lint with auto-fix
security: ## Run gosec-only scan mirroring CI (ignores .golangci.yaml, so test files are scanned too)
@golangci-lint run --no-config -E gosec ./...

test: ## Run all tests
@go test -count=1 ./...
test: ## Run all tests (with race detector)
@go test -race -count=1 ./...

test.integration: build ## Run integration tests (requires staging credentials in [test] profile)
@cp $(OUTPUT_DIR)/verda /usr/local/bin/verda-test
Expand All @@ -38,6 +38,7 @@ test-s3-integration: build ## Run S3 data-plane smoke test against a live endpoi

fmt: ## Format code with gofmt and goimports
@gofmt -w .
@# local prefix has a known typo (missing .com, same as .golangci.yaml); fixing it would regroup ~100 files — deliberate deferral, see .golangci.yaml.
@goimports -w -local github/verda-cloud/verda-cli .
@go mod tidy

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ go install github.com/verda-cloud/verda-cli/cmd/verda@latest
```bash
verda --version # verify installation
verda update # update to latest
verda update --version v1.0.0 # specific version
verda update --target v1.0.0 # specific version
```

## Getting Started
Expand Down Expand Up @@ -157,6 +157,12 @@ Once configured, just talk to your agent:

Credentials are shared with the CLI — run `verda auth login` first.

### Safety contract

Tools that create billed resources (`create_vm`, `create_volume`) require `confirm: true`, and `vm_action` requires it for the destructive actions (`shutdown`, `force_shutdown`, `hibernate`, `delete`) — the same gate as `--yes` in `--agent` mode. Without it the tool fails with `CONFIRMATION_REQUIRED` in the [agent error format](docs/agent-errors.md#mcp-server-tools), so the agent can present the exact action to the user and retry after approval.

`vm_action` reports `status: "accepted"` once the API accepts the action; only `wait: true` polls the instance to its expected status and reports `"completed"` (`create_vm` waits by default). Argument types are validated strictly — wrong JSON types and unknown enum values fail with `VALIDATION_ERROR` instead of being silently coerced. The full tool/parameter reference lives in [cmd/mcp/README.md](internal/verda-cli/cmd/mcp/README.md).

### Agent Mode

For scripts and agents that use the CLI directly (without MCP):
Expand Down
5 changes: 5 additions & 0 deletions cmd/verda/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func main() {
if err := root.Execute(); errors.Is(err, cmd.ErrVersionRequested) {
// --version flag was handled; exit cleanly.
return
} else if cmdutil.IsPromptCancel(err) {
// User cancel (Ctrl+C / Esc from a prompt or wizard) — clean exit,
// no stderr noise. Real failures propagate below. Checked before the
// agent-mode branch so a cancel stays silent there too.
return
} else if err != nil {
// In agent mode, always emit structured JSON errors.
if opts.Agent || cmdutil.IsAgentError(err) {
Expand Down
23 changes: 19 additions & 4 deletions docs/agent-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,11 @@ Catch-all for errors that don't match a more specific code.
Errors are classified in this priority order:

1. **Already an AgentError** (from explicit checks in commands) -- returned as-is
2. **SDK `APIError`** -- mapped by HTTP status code (401/403 -> AUTH_ERROR, 404 -> NOT_FOUND, 402 -> INSUFFICIENT_BALANCE, others -> API_ERROR)
3. **SDK `ValidationError`** -- mapped to VALIDATION_ERROR with field and reason
4. **Auth-related message heuristic** -- messages containing "no credentials configured", "unauthorized", "token expired" -> AUTH_ERROR
5. **Fallback** -- generic ERROR with the original message
2. **CLI usage errors** (`cmdutil.UsageError` from flag/argument misuse) -- VALIDATION_ERROR, exit 2
3. **SDK `APIError`** -- mapped by HTTP status code (401/403 -> AUTH_ERROR, 404 -> NOT_FOUND, 402 -> INSUFFICIENT_BALANCE, others -> API_ERROR)
4. **SDK `ValidationError`** -- mapped to VALIDATION_ERROR with field and reason
5. **Auth-related message heuristic** -- messages containing "no credentials configured", "unauthorized", "token expired" -> AUTH_ERROR
6. **Fallback** -- generic ERROR with the original message

## For Developers

Expand All @@ -246,3 +247,17 @@ Errors are classified in this priority order:
- Error types: `internal/verda-cli/cmd/util/agent_error.go`
- Classification: `ClassifyError()` in the same file
- Entry point: `cmd/verda/main.go` calls `ClassifyError()` on all errors

## MCP server tools

Tools exposed by `verda mcp serve` reuse this contract with one transport difference: MCP has no stderr/exit codes, so tool failures arrive as tool results with `isError: true` whose **text payload is the same JSON envelope**. Argument-contract errors produced inside the MCP server use these codes:

- `CONFIRMATION_REQUIRED` — billing/destructive tool called without `confirm: true`; `details.action` names the gated action
- `MISSING_REQUIRED_FLAGS` — required tool argument absent; `details.missing` lists them
- `VALIDATION_ERROR` — argument type/out-of-set value rejected; `details.field` + `details.reason`

```json
{"error": {"code": "CONFIRMATION_REQUIRED", "message": "action \"delete\" creates billing or destructive changes and requires an explicit confirm: true argument", "details": {"action": "delete"}}}
```

All other tool failures (API errors, auth, unknown IDs) arrive as plain-text `isError` results. See `internal/verda-cli/cmd/mcp/README.md` for the full tool reference.
5 changes: 3 additions & 2 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ Credentials are resolved from multiple sources in order of precedence:
| 3 | Environment variables | `VERDA_CLIENT_ID`, `VERDA_CLIENT_SECRET` |
| 4 | Credentials file | `[default]` in `~/.verda/credentials` |

> **Note:** When `--auth.profile` is passed explicitly, the credentials file
> values for that profile override env vars — but CLI flags still win.
> **Note:** `--auth.profile` / `VERDA_PROFILE` select WHICH credentials file
> profile fills missing values; inline sources (flags, config, env — including
> the `VERDA_AUTH_*` spellings) always win over stored profile values.

### Environment Variables

Expand Down
11 changes: 7 additions & 4 deletions internal/skills/files/verda-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ All commands: `--agent -o json` (except `verda ssh` and `verda auth show`).

**Optional flags:** `--location` (default FIN-01), `--ssh-key` (repeatable, takes ID), `--is-spot`, `--os-volume-size` (GiB), `--storage-size` (GiB), `--storage-type` (NVMe/HDD), `--startup-script` (ID), `--contract` (PAY_AS_YOU_GO/SPOT/LONG_TERM), `--from` (template name), `--wait`, `--wait-timeout` (use 2m)

**Agent-mode wait semantics:** in `--agent` mode, `vm create` and `vm action` return immediately after the API accepts the request (`status: "accepted"`). Add `--wait` to poll until the target state; the result then reports `completed`, or an error if the transition fails.

## VM Lifecycle

| Command | Key Flags |
Expand All @@ -84,7 +86,7 @@ Note: `shutdown` alias is `stop`. `delete` alias is `rm`.
| Command | Key Flags | Output Fields |
|---------|-----------|---------------|
| `verda cost balance -o json` | — | `amount`, `currency` |
| `verda cost estimate -o json` | `--type` (required), `--os-volume`, `--storage`, `--storage-type`, `--spot`, `--location` | `total.hourly`, `instance.hourly`, `os_volume.hourly` |
| `verda cost estimate -o json` | `--type` (required), `--os-volume`, `--storage`, `--storage-type`, `--spot` | `total.hourly`, `instance.hourly`, `os_volume.hourly` |
| `verda cost running -o json` | — | `instances[]` (each: `hostname`, `hourly`, `daily`, `monthly`), `total.hourly` |

## Status (Low Priority)
Expand All @@ -107,10 +109,10 @@ Tell user to run in their terminal:
|---------|-----------|
| `verda ssh-key list -o json` | — |
| `verda ssh-key add -o json` | `--name`, `--public-key` |
| `verda ssh-key delete <id> -o json` | confirm first |
| `verda ssh-key delete <id> --yes -o json` | `--yes` **required** in agent mode |
| `verda startup-script list -o json` | — |
| `verda startup-script add -o json` | `--name`, `--file` or `--script` |
| `verda startup-script delete <id> -o json` | confirm first |
| `verda startup-script delete <id> --yes -o json` | `--yes` **required** in agent mode |

## Templates (alias: `tmpl`)

Expand All @@ -135,8 +137,9 @@ Hostname patterns: `{random}` → random words, `{location}` → location code
|---------|-----------|
| `verda volume list -o json` | `--status` (attached, detached, ordered) |
| `verda volume describe <id> -o json` | — |
| `verda volume create -o json` | `--name`, `--size`, `--type` (NVMe/HDD), `--location` |
| `verda volume create -o json` | `--name`, `--size`, `--type` (NVMe/HDD), `--location`, **`--yes`** (required in agent mode — volume creation is billable) |
| `verda volume action <id>` | Actions: detach, rename, resize, clone, delete |
| `verda volume delete --id <id> --yes -o json` | `--yes` **required** in agent mode |
| `verda volume trash -o json` | Recoverable within 96 hours |

## Object Storage (S3)
Expand Down
32 changes: 31 additions & 1 deletion internal/skills/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.0.0",
"version": "1.2.0",
"skills": [
"verda-cloud.md",
"verda-reference.md"
Expand Down Expand Up @@ -36,6 +36,36 @@
"verda-cloud.md": "SKILL.md"
}
},
"kimi-code": {
"display_name": "Kimi Code",
"scope": "global",
"target": "~/.kimi-code/skills/",
"method": "copy",
"file_map": {
"verda-cloud.md": "verda-cloud/SKILL.md",
"verda-reference.md": "verda-reference/SKILL.md"
}
},
"opencode": {
"display_name": "OpenCode",
"scope": "global",
"target": "~/.config/opencode/skills/",
"method": "copy",
"file_map": {
"verda-cloud.md": "verda-cloud/SKILL.md",
"verda-reference.md": "verda-reference/SKILL.md"
}
},
"pi": {
"display_name": "Pi",
"scope": "global",
"target": "~/.pi/agent/skills/",
"method": "copy",
"file_map": {
"verda-cloud.md": "verda-cloud/SKILL.md",
"verda-reference.md": "verda-reference/SKILL.md"
}
},
"gemini": {
"display_name": "Gemini CLI",
"scope": "global",
Expand Down
21 changes: 13 additions & 8 deletions internal/verda-cli/cmd/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,28 @@ shows the order of precedence (highest first):
| `VERDA_AGENT` | Enable agent mode (`1` or `true`) |
| `VERDA_HOME` | Base directory for config (default `~/.verda`) |

### Explicit Profile Override
### Explicit Profile Selection

When `--auth.profile` is passed explicitly, the credentials file values for that
profile override env vars and config file values — but CLI flags still win.
`--auth.profile` / `VERDA_PROFILE` choose WHICH credentials file section
supplies missing values — they do not promote stored values over inline
sources. Per field, the precedence table above always applies: CLI flag >
config file (including `VERDA_AUTH_CLIENT_ID` / `VERDA_AUTH_CLIENT_SECRET` via
viper's env binding) > `VERDA_CLIENT_ID` / `VERDA_CLIENT_SECRET` > credentials
file. The selected profile additionally pins its own `verda_base_url`.

For example:

```bash
# Env var is set
export VERDA_CLIENT_ID=env-id

# Explicit profile overrides the env var
verda compute list --auth.profile=staging
# → uses client ID from [staging] in ~/.verda/credentials, not env-id
# Inline env still wins; [staging] only fills unset fields
verda vm list --auth.profile=staging
# → uses env-id (prior behavior silently used [staging]'s client ID —
# fixed after a wrong-account report)

# But a CLI flag always wins
verda compute list --auth.profile=staging --auth.client-id=flag-id
# A CLI flag wins over everything
verda vm list --auth.profile=staging --auth.client-id=flag-id
# → uses flag-id
```

Expand Down
Loading