Add guest sessions and agent VM defaults

Add daemon-backed workspace and guest-session primitives so host
orchestrators can prepare /root/repo, launch long-lived guest commands,
and attach to pipe-mode sessions over the local stdio mux bridge.

Persist richer session metadata and launch diagnostics, preflight guest
cwd/command requirements, make pipe-mode attach rehydratable from guest
state after daemon restart, and allow submodules when workspace prepare
runs in full_copy mode.

At the same time, stop vm run from auto-attaching opencode, make it
print next-step commands instead, and make glibc guest images more
agent-ready by installing node, opencode, claude, and pi while syncing
opencode/claude/pi auth files into work disks on VM start.

Validation:
- GOCACHE=/tmp/banger-gocache go test ./...
- make build
- banger vm workspace prepare --help
- banger vm session --help
- banger vm session start --help
- banger vm session attach --help
This commit is contained in:
Thales Maciel 2026-04-12 23:48:42 -03:00
parent 497e6dca3d
commit 37c4c091ec
No known key found for this signature in database
GPG key ID: 33112E6833C34679
18 changed files with 3212 additions and 405 deletions

View file

@ -35,8 +35,14 @@ const (
workDiskGitConfigRelativePath = ".gitconfig"
workDiskOpencodeAuthDirRelativePath = ".local/share/opencode"
workDiskOpencodeAuthRelativePath = workDiskOpencodeAuthDirRelativePath + "/auth.json"
workDiskClaudeAuthDirRelativePath = ".claude"
workDiskClaudeAuthRelativePath = workDiskClaudeAuthDirRelativePath + "/.credentials.json"
workDiskPiAuthDirRelativePath = ".pi/agent"
workDiskPiAuthRelativePath = workDiskPiAuthDirRelativePath + "/auth.json"
hostGlobalGitIdentitySource = "git config --global"
hostOpencodeAuthDefaultDisplayPath = "~/" + workDiskOpencodeAuthRelativePath
hostClaudeAuthDefaultDisplayPath = "~/" + workDiskClaudeAuthRelativePath
hostPiAuthDefaultDisplayPath = "~/" + workDiskPiAuthRelativePath
)
type gitIdentity struct {
@ -967,19 +973,60 @@ func (d *Daemon) ensureGitIdentityOnWorkDisk(ctx context.Context, vm *model.VMRe
}
func (d *Daemon) ensureOpencodeAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
hostAuthPath, err := resolveHostOpencodeAuthPath()
return d.ensureAuthFileOnWorkDisk(
ctx,
vm,
"syncing opencode auth",
hostOpencodeAuthDefaultDisplayPath,
resolveHostOpencodeAuthPath,
workDiskOpencodeAuthRelativePath,
d.warnOpencodeAuthSyncSkipped,
)
}
func (d *Daemon) ensureClaudeAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
return d.ensureAuthFileOnWorkDisk(
ctx,
vm,
"syncing claude auth",
hostClaudeAuthDefaultDisplayPath,
resolveHostClaudeAuthPath,
workDiskClaudeAuthRelativePath,
d.warnClaudeAuthSyncSkipped,
)
}
func (d *Daemon) ensurePiAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
return d.ensureAuthFileOnWorkDisk(
ctx,
vm,
"syncing pi auth",
hostPiAuthDefaultDisplayPath,
resolveHostPiAuthPath,
workDiskPiAuthRelativePath,
d.warnPiAuthSyncSkipped,
)
}
func (d *Daemon) ensureAuthFileOnWorkDisk(ctx context.Context, vm *model.VMRecord, stageDetail, defaultDisplayPath string, resolveHostPath func() (string, error), guestRelativePath string, warn func(model.VMRecord, string, error)) error {
hostAuthPath, err := resolveHostPath()
if err != nil {
d.warnOpencodeAuthSyncSkipped(*vm, hostOpencodeAuthDefaultDisplayPath, err)
warn(*vm, defaultDisplayPath, err)
return nil
}
authData, err := os.ReadFile(hostAuthPath)
if err != nil {
d.warnOpencodeAuthSyncSkipped(*vm, hostAuthPath, err)
warn(*vm, hostAuthPath, err)
return nil
}
vmCreateStage(ctx, "prepare_work_disk", "syncing opencode auth")
workMount, cleanupWork, err := system.MountTempDir(ctx, d.runner, vm.Runtime.WorkDiskPath, false)
runner := d.runner
if runner == nil {
runner = system.NewRunner()
}
vmCreateStage(ctx, "prepare_work_disk", stageDetail)
workMount, cleanupWork, err := system.MountTempDir(ctx, runner, vm.Runtime.WorkDiskPath, false)
if err != nil {
return err
}
@ -989,13 +1036,13 @@ func (d *Daemon) ensureOpencodeAuthOnWorkDisk(ctx context.Context, vm *model.VMR
return err
}
authDir := filepath.Join(workMount, workDiskOpencodeAuthDirRelativePath)
if _, err := d.runner.RunSudo(ctx, "mkdir", "-p", authDir); err != nil {
authDir := filepath.Join(workMount, filepath.Dir(guestRelativePath))
if _, err := runner.RunSudo(ctx, "mkdir", "-p", authDir); err != nil {
return err
}
authPath := filepath.Join(workMount, workDiskOpencodeAuthRelativePath)
authPath := filepath.Join(workMount, guestRelativePath)
tmpFile, err := os.CreateTemp("", "banger-opencode-auth-*")
tmpFile, err := os.CreateTemp("", "banger-auth-*")
if err != nil {
return err
}
@ -1011,16 +1058,28 @@ func (d *Daemon) ensureOpencodeAuthOnWorkDisk(ctx context.Context, vm *model.VMR
}
defer os.Remove(tmpPath)
_, err = d.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authPath)
_, err = runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authPath)
return err
}
func resolveHostOpencodeAuthPath() (string, error) {
return resolveHostAuthPath(workDiskOpencodeAuthRelativePath)
}
func resolveHostClaudeAuthPath() (string, error) {
return resolveHostAuthPath(workDiskClaudeAuthRelativePath)
}
func resolveHostPiAuthPath() (string, error) {
return resolveHostAuthPath(workDiskPiAuthRelativePath)
}
func resolveHostAuthPath(relativePath string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, workDiskOpencodeAuthRelativePath), nil
return filepath.Join(home, relativePath), nil
}
func resolveHostGlobalGitIdentity(ctx context.Context, runner system.CommandRunner) (gitIdentity, error) {
@ -1093,6 +1152,20 @@ func (d *Daemon) warnOpencodeAuthSyncSkipped(vm model.VMRecord, hostPath string,
d.logger.Warn("guest opencode auth sync skipped", append(vmLogAttrs(vm), "host_path", hostPath, "error", err.Error())...)
}
func (d *Daemon) warnClaudeAuthSyncSkipped(vm model.VMRecord, hostPath string, err error) {
if d.logger == nil || err == nil {
return
}
d.logger.Warn("guest claude auth sync skipped", append(vmLogAttrs(vm), "host_path", hostPath, "error", err.Error())...)
}
func (d *Daemon) warnPiAuthSyncSkipped(vm model.VMRecord, hostPath string, err error) {
if d.logger == nil || err == nil {
return
}
d.logger.Warn("guest pi auth sync skipped", append(vmLogAttrs(vm), "host_path", hostPath, "error", err.Error())...)
}
func (d *Daemon) warnGitIdentitySyncSkipped(vm model.VMRecord, source string, err error) {
if d.logger == nil || err == nil {
return