banger/internal/daemon/vm_authsync.go
Thales Maciel ea0db1e17e
Split internal/daemon vm.go and guest_sessions.go by concern
vm.go (1529 LOC) splits into vm_create, vm_lifecycle, vm_set, vm_stats,
vm_disk, vm_authsync; firecracker/DNS/helpers stay in vm.go.

guest_sessions.go (1266 LOC) splits into session_controller,
session_lifecycle, session_attach, session_stream; scripts and helpers
stay in guest_sessions.go.

Mechanical move only. No behavior change. Adds doc.go and
ARCHITECTURE.md capturing subsystem map and current lock ordering as
the baseline for the upcoming subsystem extraction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:47:08 -03:00

353 lines
10 KiB
Go

package daemon
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"banger/internal/guest"
"banger/internal/model"
"banger/internal/system"
)
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 {
Name string
Email string
}
func (d *Daemon) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm *model.VMRecord, image model.Image, prep workDiskPreparation) error {
fingerprint, err := guest.AuthorizedPublicKeyFingerprint(d.config.SSHKeyPath)
if err != nil {
return fmt.Errorf("derive authorized ssh key fingerprint: %w", err)
}
if prep.ClonedFromSeed && image.SeededSSHPublicKeyFingerprint != "" && image.SeededSSHPublicKeyFingerprint == fingerprint {
vmCreateStage(ctx, "prepare_work_disk", "using seeded SSH access")
return nil
}
publicKey, err := guest.AuthorizedPublicKey(d.config.SSHKeyPath)
if err != nil {
return fmt.Errorf("derive authorized ssh key: %w", err)
}
vmCreateStage(ctx, "prepare_work_disk", "repairing SSH access on work disk")
workMount, cleanupWork, err := system.MountTempDir(ctx, d.runner, vm.Runtime.WorkDiskPath, false)
if err != nil {
return err
}
defer cleanupWork()
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
return err
}
sshDir := filepath.Join(workMount, ".ssh")
if _, err := d.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil {
return err
}
if _, err := d.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil {
return err
}
authorizedKeysPath := filepath.Join(sshDir, "authorized_keys")
existing, err := d.runner.RunSudo(ctx, "cat", authorizedKeysPath)
if err != nil {
existing = nil
}
merged := mergeAuthorizedKey(existing, publicKey)
tmpFile, err := os.CreateTemp("", "banger-authorized-keys-*")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
if _, err := tmpFile.Write(merged); err != nil {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
return err
}
if err := tmpFile.Close(); err != nil {
_ = os.Remove(tmpPath)
return err
}
defer os.Remove(tmpPath)
if _, err := d.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil {
return err
}
if prep.ClonedFromSeed && image.Managed {
vmCreateStage(ctx, "prepare_work_disk", "refreshing managed work seed")
if err := d.refreshManagedWorkSeedFingerprint(ctx, image, fingerprint); err != nil {
return err
}
}
return nil
}
func (d *Daemon) ensureGitIdentityOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
runner := d.runner
if runner == nil {
runner = system.NewRunner()
}
identity, err := resolveHostGlobalGitIdentity(ctx, runner)
if err != nil {
d.warnGitIdentitySyncSkipped(*vm, hostGlobalGitIdentitySource, err)
return nil
}
vmCreateStage(ctx, "prepare_work_disk", "syncing git identity")
workMount, cleanupWork, err := system.MountTempDir(ctx, runner, vm.Runtime.WorkDiskPath, false)
if err != nil {
return err
}
defer cleanupWork()
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
return err
}
return writeGitIdentity(ctx, runner, filepath.Join(workMount, workDiskGitConfigRelativePath), identity)
}
func (d *Daemon) ensureOpencodeAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
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 {
warn(*vm, defaultDisplayPath, err)
return nil
}
authData, err := os.ReadFile(hostAuthPath)
if err != nil {
warn(*vm, hostAuthPath, err)
return nil
}
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
}
defer cleanupWork()
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
return err
}
authDir := filepath.Join(workMount, filepath.Dir(guestRelativePath))
if _, err := runner.RunSudo(ctx, "mkdir", "-p", authDir); err != nil {
return err
}
authPath := filepath.Join(workMount, guestRelativePath)
tmpFile, err := os.CreateTemp("", "banger-auth-*")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
if _, err := tmpFile.Write(authData); err != nil {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
return err
}
if err := tmpFile.Close(); err != nil {
_ = os.Remove(tmpPath)
return err
}
defer os.Remove(tmpPath)
_, 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, relativePath), nil
}
func resolveHostGlobalGitIdentity(ctx context.Context, runner system.CommandRunner) (gitIdentity, error) {
name, err := gitConfigValue(ctx, runner, nil, "user.name")
if err != nil {
return gitIdentity{}, err
}
if name == "" {
return gitIdentity{}, errors.New("host git user.name is empty")
}
email, err := gitConfigValue(ctx, runner, nil, "user.email")
if err != nil {
return gitIdentity{}, err
}
if email == "" {
return gitIdentity{}, errors.New("host git user.email is empty")
}
return gitIdentity{Name: name, Email: email}, nil
}
func gitConfigValue(ctx context.Context, runner system.CommandRunner, extraArgs []string, key string) (string, error) {
args := []string{"config"}
args = append(args, extraArgs...)
args = append(args, "--default", "", "--get", key)
out, err := runner.Run(ctx, "git", args...)
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
func writeGitIdentity(ctx context.Context, runner system.CommandRunner, gitConfigPath string, identity gitIdentity) error {
existing, err := runner.RunSudo(ctx, "cat", gitConfigPath)
if err != nil {
existing = nil
}
tmpFile, err := os.CreateTemp("", "banger-gitconfig-*")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
if _, err := tmpFile.Write(existing); err != nil {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
return err
}
if err := tmpFile.Close(); err != nil {
_ = os.Remove(tmpPath)
return err
}
defer os.Remove(tmpPath)
if _, err := runner.Run(ctx, "git", "config", "--file", tmpPath, "user.name", identity.Name); err != nil {
return err
}
if _, err := runner.Run(ctx, "git", "config", "--file", tmpPath, "user.email", identity.Email); err != nil {
return err
}
_, err = runner.RunSudo(ctx, "install", "-m", "644", tmpPath, gitConfigPath)
return err
}
func (d *Daemon) warnOpencodeAuthSyncSkipped(vm model.VMRecord, hostPath string, err error) {
if d.logger == nil || err == nil {
return
}
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
}
d.logger.Warn("guest git identity sync skipped", append(vmLogAttrs(vm), "source", source, "error", err.Error())...)
}
func mergeAuthorizedKey(existing, managed []byte) []byte {
managedLine := strings.TrimSpace(string(managed))
if managedLine == "" {
return append([]byte(nil), existing...)
}
lines := strings.Split(strings.ReplaceAll(string(existing), "\r\n", "\n"), "\n")
out := make([]string, 0, len(lines)+1)
found := false
for _, line := range lines {
line = strings.TrimRight(line, "\r")
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if trimmed == managedLine {
found = true
}
out = append(out, line)
}
if !found {
out = append(out, managedLine)
}
return []byte(strings.Join(out, "\n") + "\n")
}