daemon: auto-trust mise configs on workspace prepare

vm run ./repo (and the explicit vm workspace prepare) imports the
host user's own checkout. Any .mise.toml that lands in the guest
would otherwise prompt on the first guest command — 'mise trust:
hash mismatch, run "mise trust"' — and stall what should be a
zero-friction sandbox launch. The repo just came from the host,
the guest is single-tenant root@<vm>.vm, the user already trusts
this checkout: auto-trust is the right default here.

After workspaceImportHook succeeds, run
  if command -v mise >/dev/null 2>&1; then
    mise trust --quiet --all <guest_path> || true
  fi
inside the guest. Best effort: a missing mise binary, a non-zero
exit, or a no-op trust all log at debug only and never fail
prepare. The path is shell-quoted via ws.ShellQuote so guest
paths with spaces or quotes don't break the argument.

Tests pin the script shape (command -v guard + --quiet --all flag
+ trailing `|| true`) and assert the script actually fires after
a successful import. A path with an apostrophe round-trips via
ws.ShellQuote without truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-26 23:08:41 -03:00
parent fa4292756d
commit c8637b0fe4
No known key found for this signature in database
GPG key ID: 33112E6833C34679
2 changed files with 126 additions and 4 deletions

View file

@ -19,9 +19,10 @@ import (
// exportGuestClient is a scriptable fake for RunScriptOutput used in export tests.
// Each call to RunScriptOutput returns the next response from the queue.
type exportGuestClient struct {
responses []exportGuestResponse
scripts []string
callIndex int
responses []exportGuestResponse
scripts []string
callIndex int
runScriptLog []string
}
type exportGuestResponse struct {
@ -31,7 +32,8 @@ type exportGuestResponse struct {
func (e *exportGuestClient) Close() error { return nil }
func (e *exportGuestClient) RunScript(_ context.Context, _ string, _ io.Writer) error {
func (e *exportGuestClient) RunScript(_ context.Context, script string, _ io.Writer) error {
e.runScriptLog = append(e.runScriptLog, script)
return nil
}
@ -602,3 +604,83 @@ func TestExportVMWorkspace_DoesNotMutateRealIndex(t *testing.T) {
}
}
}
// TestMiseTrustScriptShape pins the exact shell run inside the
// guest by miseTrustGuestRepo. The two contracts other code paths
// rely on:
//
// 1. The script never fails the prepare — `mise trust` is wrapped
// in `... || true` and gated on `command -v mise`, so a guest
// image without mise simply no-ops.
// 2. The path is shell-quoted via ws.ShellQuote, so a guest_path
// containing spaces, quotes, or other oddballs doesn't break
// out of the argument.
func TestMiseTrustScriptShape(t *testing.T) {
got := miseTrustScript("/root/repo")
for _, want := range []string{
"command -v mise",
"mise trust --quiet --all '/root/repo'",
"|| true",
} {
if !strings.Contains(got, want) {
t.Errorf("script missing %q:\n%s", want, got)
}
}
// Path with a single quote in it must come back quoted, not
// truncated. ws.ShellQuote escapes by closing/reopening the
// quoted string around each apostrophe.
exotic := miseTrustScript("/root/it's odd")
if !strings.Contains(exotic, `'/root/it'"'"'s odd'`) {
t.Errorf("path with apostrophe was not shell-quoted safely:\n%s", exotic)
}
}
// TestPrepareVMWorkspace_RunsMiseTrustAfterImport asserts the auto-
// trust step fires once a successful import lands. Failure-path
// behaviour (no import → no trust) is covered by the existing
// rejection tests.
func TestPrepareVMWorkspace_RunsMiseTrustAfterImport(t *testing.T) {
t.Parallel()
ctx := context.Background()
apiSock := filepath.Join(t.TempDir(), "fc.sock")
firecracker := startFakeFirecracker(t, apiSock)
vm := testVM("trustbox", "image-trust", "172.16.0.211")
vm.State = model.VMStateRunning
vm.Runtime.State = model.VMStateRunning
vm.Runtime.APISockPath = apiSock
fake := &exportGuestClient{}
d := newExportTestDaemonStore(t, fake)
d.guestWaitForSSH = func(_ context.Context, _, _ string, _ time.Duration) error { return nil }
upsertDaemonVM(t, ctx, d.store, vm)
d.vm.setVMHandlesInMemory(vm.ID, model.VMHandles{PID: firecracker.Process.Pid})
d.ws.workspaceInspectRepo = func(context.Context, string, string, string, bool) (workspace.RepoSpec, error) {
return workspace.RepoSpec{RepoName: "x", RepoRoot: "/tmp/x"}, nil
}
d.ws.workspaceImport = func(context.Context, workspace.GuestClient, workspace.RepoSpec, string, model.WorkspacePrepareMode) error {
return nil
}
if _, err := d.ws.PrepareVMWorkspace(ctx, api.VMWorkspacePrepareParams{
IDOrName: vm.Name,
SourcePath: "/tmp/x",
GuestPath: "/root/repo",
}); err != nil {
t.Fatalf("PrepareVMWorkspace: %v", err)
}
var sawTrust bool
for _, script := range fake.runScriptLog {
if strings.Contains(script, "mise trust") {
sawTrust = true
break
}
}
if !sawTrust {
t.Fatalf("expected mise trust script after import; saw %d scripts: %v", len(fake.runScriptLog), fake.runScriptLog)
}
}