Skip to content

sshmgr 3.3.0: git remote modes, devbox VMs, knownhosts pin fix - #52

Merged
imanimanyara merged 10 commits into
mainfrom
feat/v3.3.0
Oct 20, 2026
Merged

imanimanyara merged 10 commits into
mainfrom
feat/v3.3.0

Conversation

@imanimanyara

Copy link
Copy Markdown
Member

sshmgr 3.3.0 (unreleased; lands under [Unreleased]): git remote modes, devbox VMs, and the knownhosts pin bug. One commit per concern.

Git remote modes

  • sshmgr git status [PATH...], and sshmgr git use profile|ssh|https [PATH...] with --mode, --profile, --dry-run and --yes.
  • Mode precedence: argument, then --mode, then SSHMGR_GIT_MODE, then manifest defaults.git.mode, then profile.
  • Owner → profile through defaults.git.orgs, then defaults.git.profile. Several candidates is an error, not a guess.
  • Each new URL must pass git ls-remote (prompts off, SSH in batch mode) before origin is rewritten. Non-GitHub origins are skipped.
  • With defaults.git.profile set, the renderer adds a Host github.com block before Host * (ssh -G pinned).

Devboxes

  • src/internal/core/hypervisor: one interface over Parallels, VMPal, VMware, UTM, Lima, OrbStack and Podman.
  • A host with vm: {hypervisor, name} renders ProxyCommand "<sshmgr>" vm proxy <alias>. vm proxy resolves the address on each connection, starts a stopped VM, and relays stdio.
  • vm list, vm add (confirms, with --yes) and vm docker-context.
  • OrbStack and Podman run their own SSH and keys, so the command, manifest validation and schema all refuse them as hosts.
  • doctor fails on a ProxyCommand whose sshmgr binary has gone (stale_proxy); config render rewrites it.

Fixes

  • knownhosts pin reported pinned 0 and exited 0 for a host serving no SSH key. It now names each such host and exits non-zero.
  • The manifest JSON schema rejected 3.2.0 fields (category, key_store, op_vault, keys, onepassword_agent). It now matches the code, and a test compares every type's JSON fields.
  • The knownhosts docs and feature catalog described per-profile 0644 stores; there is one hashed 0600 store.

Guards added or extended, each shown to fail first:

  • The pin failure.
  • Schema vs code.
  • doctor's verdict.
  • The binary-surface test, which now also reads the hypervisor drivers' lookup() names (git, docker and seven hypervisor CLIs declared).
  • The surface pin (goOnly entries for every new verb and flag).
  • Mutating verbs confirm.

Verified locally

  • gofmt and go vet pass; go build passes for linux, windows and darwin; make e2e passes.
  • The full test suite's failures are byte-identical to the known sandbox baseline: the same 10 local-socket tests.
  • TestVMProxyStartsAStoppedVMAndRelays needs a local listener, so it skipped here and runs in CI. The relay itself is also tested over net.Pipe.
  • The VMPal driver was run against the installed vmpal; the other drivers are tested with stand-in CLIs.
  • golangci-lint could not run locally (the installed binary crashes on Go 1.26.5); CI's lint step is the check.

Docs: docs/tools/git.md, docs/tools/vm.md, two recipes, configuration.md (defaults.git, vm), doctor.md, the README index and the CHANGELOG.

A reachable port that is not SSH (a web server on 443, for one) made
ssh-keyscan return nothing, and pin printed "pinned 0 host key(s)" and
exited 0, which read as success and left the host untrusted. ScanChecked
says why nothing came back, a missing ssh-keyscan included; pin reports
each such host, still pins the rest, and exits non-zero. Scan keeps its
old contract for its other callers.
defaults.git holds the remote mode, the default identity and an
owner-to-profile map; a host's vm names the hypervisor and VM whose
address is resolved at connect time. Both are omitted when unset, so
existing manifests save unchanged.

The JSON schema had drifted: category, key_store, op_vault, keys and
onepassword_agent were valid in code and rejected by the schema. A test
now compares every manifest type's JSON fields with the schema.
With defaults.git.profile set, plain git@github.com remotes resolve to
that profile's GitHub key through a Host github.com block placed before
Host *. A host with vm gets ProxyCommand "<sshmgr>" vm proxy <alias>,
written by sshmgr itself (raw_options still may not hold one), preferring
the PATH entry so a package upgrade does not strand the path. The path is
quoted literally: ssh does not unescape, so Go's %q would double Windows
backslashes. Both are pinned against ssh -G.
git use points each checkout's GitHub origin at profile SSH, plain SSH or
HTTPS. The mode is the first argument, else --mode, else SSHMGR_GIT_MODE,
else defaults.git.mode, else profile; the owner picks the profile through
defaults.git.orgs, then defaults.git.profile, and several candidates are
an error rather than a guess. A new URL must pass git ls-remote before
origin is rewritten, non-GitHub origins are skipped, and --dry-run
touches nothing. git status shows each checkout's mode and profile.
One interface over Parallels (prlctl), VMPal, VMware (vmrun), UTM
(utmctl), Lima (limactl), OrbStack (orb) and Podman: list VMs, resolve a
VM's current SSH endpoint, start it, and run a command in it where the
CLI can. OrbStack and Podman run their own SSH and keys, so they are
listed but never handed an address; VMware runs guest commands only with
guest credentials, so its Exec says so. Tests use stand-in CLIs on PATH;
the VMPal driver was also run against the installed vmpal.
vm add records a VM host (hypervisor and VM name; HostName is the alias,
since ProxyCommand ignores it and an address would go stale) and renders
its ProxyCommand. vm proxy, which ssh runs, resolves the VM's address on
each connection, starts a stopped VM and waits for it, then relays
stdio. vm list shows every hypervisor's VMs and which host uses each,
and says when a hypervisor is not installed. vm docker-context creates
or updates an ssh://<alias> Docker context. OrbStack and Podman are
refused as hosts: they run their own SSH and keys.
A package upgrade can move the binary a rendered ProxyCommand names,
and every connection to that VM then fails with a bare exec error. doctor
reads the root and category configs, reports each sshmgr ProxyCommand
whose binary is gone (a bare name resolves through PATH; foreign
ProxyCommands are ignored), fails its verdict, and says to run sshmgr
config render. stale_proxy is in the JSON, always an array.
vm add already refused them, but a hand-edited manifest could still give
one a vm block, rendering a ProxyCommand for a machine whose SSH and
keys the hypervisor owns. Validation and the schema now agree with the
command.
New reference pages for git and vm, recipes for switching checkouts to
profile SSH and for a VM devbox, the defaults.git and vm manifest fields,
doctor's stale_proxy check, and the CHANGELOG. Also corrects the
knownhosts page and feature catalog, which still described per-profile
trust stores at 0644.
The binary-surface guard flagged git and docker. It could not see the
hypervisor CLIs at all, because the drivers resolve them through
lookup() and run them from a variable; it now reads lookup's literal
arguments too, so prlctl, vmpal, vmrun, utmctl, limactl, orb and podman
are pinned (proven by removing one: the test names it). vm add changes
~/.ssh and now confirms like every other mutating verb, with --yes.
Copilot AI balanced review requested due to automatic review settings October 20, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@imanimanyara
imanimanyara merged commit 9767b9c into main Oct 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants