Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ 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.
Tools that create billed resources (`create_vm`, `create_volume`) require `confirm: true`, and `vm_action` requires it for the destructive actions (`shutdown`, `force_shutdown`, `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).

Expand Down
1 change: 0 additions & 1 deletion docs/ai-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ verda template edit my-tmpl # Edit an existing template
List my running Verda Cloud VMs
Shut down my Verda Cloud training VM
Start my Verda Cloud gpu-runner instance back up
Hibernate my Verda Cloud dev box
Delete the Verda Cloud instance I'm not using anymore
```

Expand Down
2 changes: 1 addition & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Full reference for all Verda CLI commands and flags.
| `verda vm create` | Create a VM (interactive wizard or flags) |
| `verda vm list` | List and inspect VM instances |
| `verda vm describe` | Show detailed info about a single VM |
| `verda vm action` | Start, shutdown, hibernate, or delete a VM |
| `verda vm action` | Start, shutdown, or delete a VM |
| `verda vm availability` | Show available instance types with pricing |

## SSH
Expand Down
2 changes: 0 additions & 2 deletions internal/skills/files/verda-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ All commands: `--agent -o json` (except `verda ssh` and `verda auth show`).
| "VM info", "instance info", "describe", "show VM" | `vm describe <id>` |
| "start", "boot", "power on" | `vm start <id>` |
| "stop", "shut down", "power off" | `vm shutdown <id>` (alias: `stop`) |
| "hibernate", "suspend", "sleep" | `vm hibernate <id>` |
| "delete VM", "delete instance", "remove", "destroy", "terminate" | `vm delete <id>` (alias: `rm`) |
| "template", "saved config", "preset", "my templates" | `template list` (alias: `tmpl`) |
| "deploy from template", "use template", "quick deploy" | `vm create --from <name>` |
Expand Down Expand Up @@ -73,7 +72,6 @@ All commands: `--agent -o json` (except `verda ssh` and `verda auth show`).
| `verda vm describe <id> -o json` | — |
| `verda vm start <id> --wait` | `--yes` in agent mode |
| `verda vm shutdown <id> --wait` | `--yes` in agent mode. Alias: `stop` |
| `verda vm hibernate <id> --wait` | `--yes` in agent mode |
| `verda vm delete <id> --wait` | `--yes` **required**. `--with-volumes` to also delete attached volumes. Alias: `rm` |

Batch operations: `--all` with `--status` and/or `--hostname` (glob pattern) to target multiple VMs.
Expand Down
2 changes: 1 addition & 1 deletion internal/verda-cli/cmd/mcp/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

### Confirm gates (hard requirement, mirrors `--yes`)
- Billing: `create_vm`, `create_volume` — `confirm: true` always required (schema marks it Required, but mcp-go does not enforce schemas — the handler gate is the enforcement)
- Destructive `vm_action`: `shutdown`, `force_shutdown`, `hibernate`, `delete` — gated via the `vmActions` table's `destructive` flag
- Destructive `vm_action`: `shutdown`, `force_shutdown`, `delete` — gated via the `vmActions` table's `destructive` flag
- The gate runs BEFORE `verdaClient()`/API calls; a refused call must have zero side effects
- Agent-facing codes: `CONFIRMATION_REQUIRED`, `VALIDATION_ERROR`, `MISSING_REQUIRED_FLAGS` — same names as the CLI `--agent` contract

Expand Down
6 changes: 3 additions & 3 deletions internal/verda-cli/cmd/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Credentials are shared with the CLI — run `verda auth login` first in the same

## Confirmation contract

Tools that create billed resources (`create_vm`, `create_volume`) or perform destructive actions (`vm_action` with `shutdown`, `force_shutdown`, `hibernate`, `delete`) require the boolean argument `confirm: true`. This mirrors `--yes` in the CLI's `--agent` mode.
Tools that create billed resources (`create_vm`, `create_volume`) or perform destructive actions (`vm_action` with `shutdown`, `force_shutdown`, `delete`) require the boolean argument `confirm: true`. This mirrors `--yes` in the CLI's `--agent` mode.

Without it, the tool fails with `isError: true` and a JSON payload following the CLI agent-error envelope (see [docs/agent-errors.md](../../../docs/agent-errors.md)):

Expand All @@ -54,7 +54,7 @@ Without it, the tool fails with `isError: true` and a JSON payload following the

`vm_action` reports `status: "accepted"` by default — the API has accepted the action, nothing more. `shutdown` of a running VM is *not* done at that point; do not tell the user billing stopped.

With `wait: true`, the tool polls the instance (up to 5 minutes) until it reaches the action's expected status (`start`→running, `shutdown`/`force_shutdown`/`hibernate`→offline) and then reports `status: "completed"` plus `instance_status`. A failed transition (instance enters `error`) is a tool error, not a success. `delete` is not polled and always returns `accepted`.
With `wait: true`, the tool polls the instance (up to 5 minutes) until it reaches the action's expected status (`start`→running, `shutdown`/`force_shutdown`→offline) and then reports `status: "completed"` plus `instance_status`. A failed transition (instance enters `error`) is a tool error, not a success. `delete` is not polled and always returns `accepted`.

`create_vm` keeps `wait: true` as its default and blocks until the instance is `running`; on poll failure it returns the instance plus `poll_error`/`poll_timed_out` fields.

Expand All @@ -64,7 +64,7 @@ mcp-go does not validate arguments against the declared schema. All handlers typ

- numbers must be numbers — `"500"` for `os_volume_size_gb` is rejected (was: coerced to 0, silently applying the 50 GB default)
- arrays must be arrays — `ssh_key_ids` as a bare string is rejected (was: dropped, attaching *all* account SSH keys)
- enums are checked against their allowed sets — `storage_type` accepts only `NVMe`/`HDD`; `vm_action` `action` accepts only the five documented verbs; unknown `estimate_cost` `storage_type` errors listing the types from the API catalog instead of pricing storage at $0
- enums are checked against their allowed sets — `storage_type` accepts only `NVMe`/`HDD`; `vm_action` `action` accepts only the four documented verbs; unknown `estimate_cost` `storage_type` errors listing the types from the API catalog instead of pricing storage at $0
- missing required arguments produce `MISSING_REQUIRED_FLAGS`

## Files
Expand Down
2 changes: 1 addition & 1 deletion internal/verda-cli/cmd/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func NewLazyServer(getClient clientFunc) *Server {
const serverInstructions = `Verda Cloud: GPU/CPU instances, volumes, SSH keys, object storage.

CONFIRM GATE — tools that create billing or destructive changes (create_vm,
create_volume, and vm_action with shutdown/force_shutdown/hibernate/delete)
create_volume, and vm_action with shutdown/force_shutdown/delete)
refuse to run unless you pass confirm: true. Show the user the exact target and
its cost first, then retry with confirm. A refused call has no side effects.

Expand Down
2 changes: 1 addition & 1 deletion internal/verda-cli/cmd/mcp/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func TestCreateVMConfirmGate(t *testing.T) {
func TestVMActionConfirmGate(t *testing.T) {
t.Parallel()

destructive := []string{"shutdown", "force_shutdown", "hibernate", "delete"}
destructive := []string{"shutdown", "force_shutdown", "delete"}
for _, action := range destructive {
t.Run(action, func(t *testing.T) {
t.Parallel()
Expand Down
13 changes: 4 additions & 9 deletions internal/verda-cli/cmd/mcp/tools_vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ func (s *Server) registerVMTools() {

s.mcpServer.AddTool(
mcp.NewTool("vm_action",
mcp.WithDescription("Perform an action on a VM: start, shutdown, force_shutdown, hibernate, or delete. Destructive actions (shutdown, force_shutdown, hibernate, delete) REQUIRE confirm: true — confirm with the user first; without it the tool fails with CONFIRMATION_REQUIRED. Returns status 'accepted' once the API has accepted the action; pass wait: true to poll until the instance reaches its expected status and report status 'completed' (a failed transition, e.g. the instance entering 'error', is a tool error). delete is not polled and always returns 'accepted'."),
mcp.WithDescription("Perform an action on a VM: start, shutdown, force_shutdown, or delete. Destructive actions (shutdown, force_shutdown, delete) REQUIRE confirm: true — confirm with the user first; without it the tool fails with CONFIRMATION_REQUIRED. Returns status 'accepted' once the API has accepted the action; pass wait: true to poll until the instance reaches its expected status and report status 'completed' (a failed transition, e.g. the instance entering 'error', is a tool error). delete is not polled and always returns 'accepted'."),
mcp.WithString("id", mcp.Required(), mcp.Description("Instance ID")),
mcp.WithString("action", mcp.Required(), mcp.Description("Action: start, shutdown, force_shutdown, hibernate, delete")),
mcp.WithBoolean("confirm", mcp.Description("Set true to confirm destructive actions (shutdown, force_shutdown, hibernate, delete). Not needed for start.")),
mcp.WithString("action", mcp.Required(), mcp.Description("Action: start, shutdown, force_shutdown, delete")),
mcp.WithBoolean("confirm", mcp.Description("Set true to confirm destructive actions (shutdown, force_shutdown, delete). Not needed for start.")),
mcp.WithBoolean("wait", mcp.Description("Poll until the instance reaches the action's expected status (default false: return 'accepted' immediately)")),
),
s.handleVMAction,
Expand Down Expand Up @@ -417,7 +417,7 @@ type vmAction struct {
}

// vmActions mirrors the CLI's vm action table (cmd/vm/action.go): ExpectStatus
// and the destructive set (shutdown/force_shutdown/hibernate carry warnings
// and the destructive set (shutdown/force_shutdown carry warnings
// there; delete is special-cased) must stay in sync.
var vmActions = map[string]vmAction{
verda.ActionStart: {
Expand All @@ -434,11 +434,6 @@ var vmActions = map[string]vmAction{
destructive: true,
exec: func(ctx context.Context, c *verda.Client, id string) error { return c.Instances.ForceShutdown(ctx, id) },
},
verda.ActionHibernate: {
expectStatus: verda.StatusOffline,
destructive: true,
exec: func(ctx context.Context, c *verda.Client, id string) error { return c.Instances.Hibernate(ctx, id) },
},
verda.ActionDelete: {
destructive: true,
exec: func(ctx context.Context, c *verda.Client, id string) error {
Expand Down
4 changes: 2 additions & 2 deletions internal/verda-cli/cmd/vm/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Quick Reference
- Parent: `verda vm` (aliases: `instance`, `instances`)
- Subcommands: `create`, `list` (alias `ls`), `describe`, `availability`, `action`, plus shortcuts (`start`, `shutdown`, `hibernate`, `delete`)
- Subcommands: `create`, `list` (alias `ls`), `describe`, `availability`, `action`, plus shortcuts (`start`, `shutdown`, `delete`)
- Files:
- `vm.go` -- Parent command, registers subcommands and shortcuts
- `create.go` -- Create command, flags, `createOptions` struct, request building, validation
Expand Down Expand Up @@ -59,7 +59,7 @@

### Action Availability
- Start: only from `offline`
- Shutdown / Force shutdown / Hibernate: only from `running`
- Shutdown / Force shutdown: only from `running`
- Delete: always available (no ValidFrom filter)
- Delete sub-flow: fetches attached volumes, lets user multi-select which to delete, warns about continued billing for undeleted volumes

Expand Down
6 changes: 2 additions & 4 deletions internal/verda-cli/cmd/vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Parent command for creating and managing Verda Cloud VM instances (GPU and CPU).
| `verda vm action` | Perform actions on a VM instance | `--id` |
| `verda vm start` | Start an offline instance | `--all`, `--status`, `--hostname` |
| `verda vm shutdown` | Shutdown a running instance | `--all`, `--status`, `--hostname` |
| `verda vm hibernate` | Hibernate a running instance | `--all`, `--status`, `--hostname` |
| `verda vm delete` | Delete an instance | `--all`, `--status`, `--hostname`, `--with-volumes` |

Aliases: `verda instance`, `verda instances`
Expand Down Expand Up @@ -127,7 +126,6 @@ Interactive instance selector appears when `--id` is not provided. Available act
| Start | offline |
| Shutdown | running |
| Force shutdown | running |
| Hibernate | running |
| Delete instance | always |

Destructive actions (Shutdown, Force shutdown, Delete) show confirmation prompts with warnings. Delete has a sub-flow for selecting which attached volumes to also delete.
Expand All @@ -148,7 +146,7 @@ Destructive actions (Shutdown, Force shutdown, Delete) show confirmation prompts
- **availability.go** -- `vm availability` with pricing table and spot price column
- **action.go** -- `vm action` command, `instanceAction` data-driven action definitions, status-filtered action menu, confirmation flows, delete sub-flow with volume selection
- **batch.go** -- Batch operations (`--all`, `--hostname` glob), per-instance error reporting, agent-mode structured JSON output
- **shortcuts.go** -- Generated shortcut commands (`vm start`, `vm shutdown`, `vm hibernate`, `vm delete`) from `shortcutDef` structs
- **shortcuts.go** -- Generated shortcut commands (`vm start`, `vm shutdown`, `vm delete`) from `shortcutDef` structs
- **instances.go** -- Shared `fetchInstances` helper with spinner and status filtering, `filterByStatus`
- **status_view.go** -- Instance card renderer, status-to-color mappings

Expand All @@ -157,7 +155,7 @@ Destructive actions (Shutdown, Force shutdown, Delete) show confirmation prompts
- `client.Instances.Create` -- Create instance
- `client.Instances.Get` -- List instances (with optional status filter)
- `client.Instances.GetByID` -- Get instance details
- `client.Instances.Start` / `Shutdown` / `ForceShutdown` / `Hibernate` -- Instance lifecycle
- `client.Instances.Start` / `Shutdown` / `ForceShutdown` -- Instance lifecycle
- `client.Instances.Delete` -- Delete instance with optional volume IDs
- `client.Instances.Action` -- Bulk action on multiple instances
- `client.InstanceAvailability.GetAllAvailabilities` -- Location/instance type availability
Expand Down
17 changes: 3 additions & 14 deletions internal/verda-cli/cmd/vm/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ var actionNameMap = map[string]string{
verda.ActionShutdown: "Shutdown",
verda.ActionForceShutdown: "Force shutdown",
"force-shutdown": "Force shutdown", // CLI alias (hyphenated)
verda.ActionHibernate: "Hibernate",
verda.ActionDelete: "Delete instance",
}

Expand Down Expand Up @@ -77,16 +76,6 @@ var allActions = []instanceAction{
return c.Instances.ForceShutdown(ctx, inst.ID)
},
},
{
Label: "Hibernate",
ValidFrom: []string{verda.StatusRunning},
ConfirmMsg: "Hibernating the instance saves its state and stops billing.\n" +
" You can resume it later.",
ExpectStatus: verda.StatusOffline,
Execute: func(ctx context.Context, c *verda.Client, inst *verda.Instance) error {
return c.Instances.Hibernate(ctx, inst.ID)
},
},
{
Label: "Delete instance",
Execute: nil, // handled specially in runAction via runDeleteFlow
Expand Down Expand Up @@ -131,7 +120,7 @@ func NewCmdAction(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command
Short: "Perform actions on a VM instance",
Long: cmdutil.LongDesc(`
Select a VM instance and perform an action: start, shutdown,
force shutdown, hibernate, or delete.
force shutdown, or delete.
`),
Example: cmdutil.Examples(`
# Interactive: select instance then action
Expand All @@ -154,7 +143,7 @@ func NewCmdAction(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command
}

cmd.Flags().StringVar(&opts.InstanceID, "id", "", "Instance ID to act on")
cmd.Flags().StringVar(&opts.Action, "action", "", "Action to perform: start, shutdown, force_shutdown, hibernate, delete")
cmd.Flags().StringVar(&opts.Action, "action", "", "Action to perform: start, shutdown, force_shutdown, delete")
cmd.Flags().BoolVar(&opts.Yes, "yes", false, "Skip confirmation for destructive actions (required in agent mode)")
cmd.Flags().BoolVar(&opts.WithVolumes, "with-volumes", false, "Also delete all attached volumes (delete only); without it the OS volume survives detached and keeps billing")
// Hidden like on non-delete shortcuts: `vm delete --with-volumes` is the
Expand Down Expand Up @@ -575,7 +564,7 @@ func validFromForAction(actionName string) []string {
func resolveAction(actionName string, validActions []instanceAction) (instanceAction, error) {
label, ok := actionNameMap[strings.ToLower(actionName)]
if !ok {
return instanceAction{}, fmt.Errorf("unknown --action %q: valid actions are start, shutdown, force_shutdown, hibernate, delete", actionName)
return instanceAction{}, fmt.Errorf("unknown --action %q: valid actions are start, shutdown, force_shutdown, delete", actionName)
}
for _, a := range validActions {
if a.Label == label {
Expand Down
3 changes: 0 additions & 3 deletions internal/verda-cli/cmd/vm/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ func TestAvailableActions_Running(t *testing.T) {
if !labels["Force shutdown"] {
t.Error("expected Force shutdown action for running instances")
}
if !labels["Hibernate"] {
t.Error("expected Hibernate action for running instances")
}
if !labels["Delete instance"] {
t.Error("expected Delete action (always available)")
}
Expand Down
2 changes: 0 additions & 2 deletions internal/verda-cli/cmd/vm/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,6 @@ func actionNameToAPI(action string) string {
return verda.ActionShutdown
case "force_shutdown", "force-shutdown":
return verda.ActionForceShutdown
case "hibernate":
return verda.ActionHibernate
case actionNameDelete:
return verda.ActionDelete
default:
Expand Down
5 changes: 2 additions & 3 deletions internal/verda-cli/cmd/vm/batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ func TestShortcutCommandsHaveBatchFlags(t *testing.T) {
vmCmd := NewCmdVM(f, ioStreams)

// The shortcut commands to check. Some may be aliases.
wantCommands := []string{"shutdown", "start", "hibernate", "delete"}
wantCommands := []string{"shutdown", "start", "delete"}
for _, name := range wantCommands {
t.Run(name, func(t *testing.T) {
var found *cobra.Command
Expand Down Expand Up @@ -434,7 +434,7 @@ func TestHostnameFlag(t *testing.T) {

vmCmd := NewCmdVM(f, ioStreams)

for _, name := range []string{"shutdown", "start", "hibernate", "delete"} {
for _, name := range []string{"shutdown", "start", "delete"} {
t.Run(name, func(t *testing.T) {
var found *cobra.Command
for _, sub := range vmCmd.Commands() {
Expand Down Expand Up @@ -462,7 +462,6 @@ func TestActionNameToAPI(t *testing.T) {
}{
{"start", verda.ActionStart},
{"shutdown", verda.ActionShutdown},
{"hibernate", verda.ActionHibernate},
{"delete", verda.ActionDelete},
{"force_shutdown", verda.ActionForceShutdown},
{"force-shutdown", verda.ActionForceShutdown},
Expand Down
5 changes: 0 additions & 5 deletions internal/verda-cli/cmd/vm/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ var shortcuts = []shortcutDef{
Short: "Shutdown a VM instance",
Action: verda.ActionShutdown,
},
{
Use: "hibernate <instance-id>",
Short: "Hibernate a VM instance",
Action: verda.ActionHibernate,
},
{
Use: "delete <instance-id>",
Aliases: []string{"rm"},
Expand Down
Loading