feat(servers): select an existing account SSH key for Managed VPS creation - #39
Conversation
…ation Adds --key-pair-identifier to `dhq servers create`, so a Managed VPS can be provisioned with an SSH key the operator already has instead of the shared key DeployHQ auto-creates. That matters for deterministic recovery access: without it there is no way to guarantee you hold the private key for a droplet. The value is the PUBLIC identifier from `dhq ssh-keys list`. The backend resolves it against the account's own keys and rejects an unknown or foreign identifier with 422, creating neither a server nor a hosted resource. The internal database id is never accepted from a client. Sent as a top-level provisioning param, a sibling of `server`, next to region/size/os_image — the backend reads params[:key_pair_identifier], not params[:server][...]. Nesting it would be a silent no-op, since Rails' permit list for Servers::ManagedVps drops it and still returns 2xx. Note this deliberately does NOT reuse --global-key-pair-id as the ticket originally proposed. That flag serialises inside `server` and is not permitted for Servers::ManagedVps, so it is a silent no-op there today; making it work would need one field serialised into two different envelope positions depending on protocol, which the SDK has no precedent for. The two are mutually exclusive and both misuses are rejected locally, before any request. Read-back adds ManagedVPSInfo.SSHKey so `dhq servers show --json managed_vps` confirms which key was applied — identifier, title and fingerprint only, never key material or an internal id. Also corrects the agent skill: global-resources.md documented `ssh-keys create --name --public-key`, and neither flag exists. Keys are generated server-side; the flags are --title and --type. Documents `ssh-keys download -o` and `ssh-keys delete` too, and warns against writing a private key to stdout. Requires the matching API change (deployhq/deployhq). Suite 871 -> 879, clean under -race and go vet. Refs DHQ-692 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz
WalkthroughThe CLI supports ChangesManaged VPS SSH key selection
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ServersCommand
participant SDK
participant ManagedVPSAPI
User->>ServersCommand: Run servers create with --key-pair-identifier
ServersCommand->>ServersCommand: Validate protocol and conflicting flags
ServersCommand->>SDK: CreateServer with KeyPairIdentifier
SDK->>ManagedVPSAPI: Send top-level key_pair_identifier
ManagedVPSAPI-->>SDK: Return ManagedVPSInfo with ssh_key
SDK-->>User: Show created server
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/commands/servers_test.go`:
- Around line 612-657: Update the three tests in
TestServersCreate_KeyPairIdentifierRejectedForNonVPS_NoHTTP,
TestServersCreate_KeyPairIdentifierConflictsWithGlobalKeyPairID_NoHTTP, and
TestServersCreate_ManagedVPSSendsKeyPairIdentifierTopLevel to use a recorded
httptest.NewServer instead of blockNetwork and captureRequest. Configure the
command host to the test server, return the appropriate recorded response,
assert zero handler calls for validation failures, and record/assert the
successful request body in the handler.
In `@internal/commands/servers.go`:
- Around line 443-459: Update both validation errors in the keyPairIdentifier
checks to include structured Recovery actions describing how to correct the
invalid flag usage, while preserving their existing messages and hints. Ensure
the recovery data is emitted for JSON and represented appropriately in non-JSON
non-interactive output, and add tests covering both protocol-type and mutually
exclusive flag failures.
In `@pkg/sdk/managed_vps_key_test.go`:
- Around line 62-87: Update TestServer_ManagedVPS_SSHKeyReadBack to load and
unmarshal the recorded API JSON golden fixture instead of using the inline raw
payload. Preserve the existing assertions for ManagedVPS.SSHKey fields and keep
TestServer_ManagedVPS_SSHKeyAbsent validating that an omitted ssh_key remains
nil.
In `@skill-evals/deployhq/evals.json`:
- Around line 635-651: Update the `create-managed-vps-with-existing-key`
expected flags to include `--name ops`, `--region lon1`, and `--size
s-1vcpu-1gb` alongside the existing flags. Update `verify-managed-vps-key` so
its expected flags place `--json` before `managed_vps`, keeping `managed_vps` as
the command’s mode argument rather than an extra positional argument.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c0a30148-d0ab-4920-9663-ee0e9541c9b6
📒 Files selected for processing (9)
CHANGELOG.mdinternal/commands/servers.gointernal/commands/servers_test.gopkg/sdk/managed_vps_key_test.gopkg/sdk/servers.gopkg/sdk/types.goskill-evals/deployhq/evals.jsonskills/deployhq/references/global-resources.mdskills/deployhq/references/servers.md
|
@codex review this |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f5dbe41bb7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
`--json` has NoOptDefVal="true" (internal/commands/root.go:160), so pflag never consumes a following token. The space form therefore leaves the field list as a positional argument: dhq servers show srv-001 -p my-app --json managed_vps → "error": "accepts 1 arg(s), received 2" (servers show is ExactArgs(1)) dhq servers show srv-001 -p my-app --json=managed_vps → reaches the API Verified against a built binary, both forms. Codex flagged the one line this PR added, but the same broken form shipped in six other places in the previous commit, and on commands without an Args validator it fails more quietly still — the field list is silently discarded and every field is returned. SKILL.md:44 was teaching the wrong form repo-wide. Fixes all 11 occurrences in the three skill files this PR already touches: 8 in references/servers.md, 2 in SKILL.md, 1 in references/global-resources.md. Bare `--json` is untouched. Two occurrences remain in references/deployments.md:62 and references/projects.md:10 — both the silent-drop class, both pre-existing, and both in files outside this PR. Left for a follow-up rather than widening the diff. Caught by Codex on #39. Refs DHQ-692 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz
Summary
Adds
--key-pair-identifiertodhq servers create, so a Managed VPS can be provisioned with an SSH key the operator already holds rather than the shared key DeployHQ auto-creates. That matters for deterministic recovery access — without it there is no guarantee you hold the private key for a droplet you own.The value is the public identifier from
dhq ssh-keys list. The backend resolves it against the account's own keys; unknown or foreign identifiers return 422 and create nothing.Companion API PR: deployhq/deployhq#1107 (also carries the DHQ-696 security fix this relies on)
Part of DHQ-692.
Deviation from the ticket — please sanity-check
The ticket says to reuse
--global-key-pair-id. I didn't, because it cannot work as written:--global-key-pair-idserialises inside theserverobjectServers::ManagedVps's permit list omits it, andaction_on_unpermitted_parametersis unset in productiondhq servers create --protocol-type managed_vps --global-key-pair-id …is a silent no-op: 200 OK, param dropped, default key usedMaking it work would mean serialising one field into two different envelope positions depending on protocol — no precedent in this SDK, and it breaks the day someone permits the field on
ManagedVps. A distinct top-level flag matches how the provisioner actually reads params. The two are mutually exclusive and both misuses are rejected locally.Review round
All five threads resolved. One fix, four skips with rationale.
--json <fields>parses the field list as a positional arg and hard-failsc7b4792— all 11 occurrences across the three skill fileshttptest.NewServerat the command layerclient.go:101hardcodeshttps://%s.deployhq.com, so a command-level test resolves tohttps://x.127.0.0.1:PORT. The SDK tests here already usehttptestintegration_test.go:16-45), andssh_keyisn't in production yet so there is nothing to recordRecoveryactionsUserErrorhas no such field; would mean threading one through 155 call sites in 49 files. Both errors setHint, the mechanism this repo usesexpected.flagsis a subset assertion by design; the case guards flag choice, not completenessThe
--jsonfix is worth noting beyond the one line Codex flagged: the same broken form shipped in six other places in the previous commit, andSKILL.md:44was teaching it repo-wide. Two occurrences remain indeployments.mdandprojects.md— silent-drop class, pre-existing, files outside this PR.Blast radius
pkg/sdk/types.goServerCreateRequest.KeyPairIdentifier(json:"-", hoisted);ManagedVPSInfo.SSHKey+ManagedVPSSSHKeypkg/sdk/servers.goCreateServer, beside region/size/os_imageinternal/commands/servers.goskills/,skill-evals/,CHANGELOG.mdAdditive throughout — omitting the flag leaves every existing path byte-identical.
Also fixes a wrong agent-skill doc
references/global-resources.mddocumenteddhq ssh-keys create --name --public-key. Neither flag exists, and it implied you supply your own public key — keys are generated server-side. Real interface is--title(required) and--type(ED25519default /RSA). Also documentsssh-keys download -oandssh-keys delete, and warns against writing a private key to stdout.This overlaps DHQ-695 (agent-skill command drift) — flagging in case that ticket should own it instead.
Test plan
go build ./cmd/dhq/,go vet ./...go test ./...— 879 passing (was 871)go test -race ./...— 879 passingNew coverage:
serverand thatkey_pair_idis never sentssh_keystaysnilrather than a zero-valued structcreateand not onupdate; non-managed_vpsprotocol and--global-key-pair-idconflict both rejected with zero HTTP (network tripwire)Staging, once #1107 is deployed:
🤖 Generated with Claude Code
https://claude.ai/code/session_01Hibw5xsRNGDkYzr1hXsDQz
Summary by CodeRabbit
--key-pair-identifier.