file_sync: config-driven replacement for hardcoded auth sync

Replace the three hardcoded host→guest credential syncs (opencode,
claude, pi) with a generic `[[file_sync]]` config list. Default is
empty — users opt in to exactly what they want synced, with no
surprise about which tools banger "supports".

```toml
[[file_sync]]
host = "~/.local/share/opencode/auth.json"
guest = "~/.local/share/opencode/auth.json"

[[file_sync]]
host = "~/.aws"          # directories are copied recursively
guest = "~/.aws"

[[file_sync]]
host = "~/bin/my-script"
guest = "~/bin/my-script"
mode = "0755"            # optional; default 0600 for files
```

Semantics:
- Host `~/...` expands against the host user's $HOME. Absolute host
  paths are used as-is.
- Guest must live under `~/` or `/root/...` — banger's work disk is
  mounted at /root in the guest, so that's the syncable namespace.
  Anything outside is rejected at config load.
- Validation at config load: reject empty paths, relative paths,
  `..` traversal, `~user/...`, malformed mode strings. Errors name
  the offending entry index.
- Missing host paths are a soft skip with a warn log (existing
  behaviour). Other errors (read, mkdir, install) abort VM create.
- File entries: `install -o 0 -g 0 -m <mode>` (default 0600).
- Directory entries: walked in Go; each source file is installed
  with its own source permissions preserved. The entry's `mode` is
  ignored for directories.

Removed (all dead after this):
- `ensureOpencodeAuthOnWorkDisk`, `ensureClaudeAuthOnWorkDisk`,
  `ensurePiAuthOnWorkDisk`, the shared `ensureAuthFileOnWorkDisk`,
  their `warn*Skipped` helpers, `resolveHost{Opencode,Claude,Pi}AuthPath`,
  and the work-disk relative-path + default display-path constants.
- The capability hook registering the three syncs now calls the
  generic `runFileSync` once.

Seven tests exercising the old codepath deleted; six new tests cover
the new runFileSync (no-op on empty config, file copy, custom mode,
missing-host-skip, overwrite, recursive directory). Config-layer
test adds happy-path parsing and a case-per-shape table of invalid
entries (empty, relative host, guest outside /root, '..' traversal,
`~user`, bad mode).

README updated: replaces the "Credential sync" section with a
"File sync" section showing the new config shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-18 16:40:11 -03:00
parent 843314be5e
commit 0933deaeb1
No known key found for this signature in database
GPG key ID: 33112E6833C34679
7 changed files with 572 additions and 398 deletions

View file

@ -923,300 +923,210 @@ func TestEnsureGitIdentityOnWorkDiskWarnsAndSkipsWhenHostIdentityIncomplete(t *t
}
}
func TestEnsureOpencodeAuthOnWorkDiskCopiesHostAuth(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
hostAuthPath := filepath.Join(homeDir, workDiskOpencodeAuthRelativePath)
if err := os.MkdirAll(filepath.Dir(hostAuthPath), 0o755); err != nil {
t.Fatalf("MkdirAll(host auth dir): %v", err)
}
hostAuth := []byte("{\"provider\":\"openai\"}\n")
if err := os.WriteFile(hostAuthPath, hostAuth, 0o600); err != nil {
t.Fatalf("WriteFile(host auth): %v", err)
}
workDiskDir := t.TempDir()
func TestRunFileSyncNoOpWhenConfigEmpty(t *testing.T) {
d := &Daemon{runner: &filesystemRunner{t: t}}
vm := testVM("auth-sync", "image-auth-sync", "172.16.0.63")
vm.Runtime.WorkDiskPath = workDiskDir
if err := d.ensureOpencodeAuthOnWorkDisk(context.Background(), &vm); err != nil {
t.Fatalf("ensureOpencodeAuthOnWorkDisk: %v", err)
}
guestAuthPath := filepath.Join(workDiskDir, workDiskOpencodeAuthRelativePath)
got, err := os.ReadFile(guestAuthPath)
if err != nil {
t.Fatalf("ReadFile(guest auth): %v", err)
}
if string(got) != string(hostAuth) {
t.Fatalf("guest auth = %q, want %q", string(got), string(hostAuth))
}
info, err := os.Stat(guestAuthPath)
if err != nil {
t.Fatalf("Stat(guest auth): %v", err)
}
if gotMode := info.Mode().Perm(); gotMode != 0o600 {
t.Fatalf("guest auth mode = %o, want 600", gotMode)
vm := testVM("no-sync", "image", "172.16.0.70")
if err := d.runFileSync(context.Background(), &vm); err != nil {
t.Fatalf("runFileSync: %v", err)
}
}
func TestEnsureOpencodeAuthOnWorkDiskReplacesExistingGuestAuth(t *testing.T) {
func TestRunFileSyncCopiesFile(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
hostAuthPath := filepath.Join(homeDir, workDiskOpencodeAuthRelativePath)
if err := os.MkdirAll(filepath.Dir(hostAuthPath), 0o755); err != nil {
t.Fatalf("MkdirAll(host auth dir): %v", err)
srcPath := filepath.Join(homeDir, ".secrets", "token")
if err := os.MkdirAll(filepath.Dir(srcPath), 0o755); err != nil {
t.Fatal(err)
}
hostAuth := []byte("{\"token\":\"fresh\"}\n")
if err := os.WriteFile(hostAuthPath, hostAuth, 0o600); err != nil {
t.Fatalf("WriteFile(host auth): %v", err)
}
workDiskDir := t.TempDir()
guestAuthPath := filepath.Join(workDiskDir, workDiskOpencodeAuthRelativePath)
if err := os.MkdirAll(filepath.Dir(guestAuthPath), 0o755); err != nil {
t.Fatalf("MkdirAll(guest auth dir): %v", err)
}
if err := os.WriteFile(guestAuthPath, []byte("{\"token\":\"stale\"}\n"), 0o600); err != nil {
t.Fatalf("WriteFile(guest auth): %v", err)
}
d := &Daemon{runner: &filesystemRunner{t: t}}
vm := testVM("auth-replace", "image-auth-replace", "172.16.0.64")
vm.Runtime.WorkDiskPath = workDiskDir
if err := d.ensureOpencodeAuthOnWorkDisk(context.Background(), &vm); err != nil {
t.Fatalf("ensureOpencodeAuthOnWorkDisk: %v", err)
}
got, err := os.ReadFile(guestAuthPath)
if err != nil {
t.Fatalf("ReadFile(guest auth): %v", err)
}
if string(got) != string(hostAuth) {
t.Fatalf("guest auth = %q, want %q", string(got), string(hostAuth))
}
}
func TestEnsureOpencodeAuthOnWorkDiskWarnsAndSkipsWhenHostAuthMissing(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
workDiskDir := t.TempDir()
guestAuthPath := filepath.Join(workDiskDir, workDiskOpencodeAuthRelativePath)
if err := os.MkdirAll(filepath.Dir(guestAuthPath), 0o755); err != nil {
t.Fatalf("MkdirAll(guest auth dir): %v", err)
}
original := []byte("{\"token\":\"keep\"}\n")
if err := os.WriteFile(guestAuthPath, original, 0o600); err != nil {
t.Fatalf("WriteFile(guest auth): %v", err)
}
var buf bytes.Buffer
logger, _, err := newDaemonLogger(&buf, "info")
if err != nil {
t.Fatalf("newDaemonLogger: %v", err)
srcData := []byte(`{"token":"abc"}`)
if err := os.WriteFile(srcPath, srcData, 0o600); err != nil {
t.Fatal(err)
}
workDisk := t.TempDir()
d := &Daemon{
runner: &filesystemRunner{t: t},
logger: logger,
config: model.DaemonConfig{
FileSync: []model.FileSyncEntry{
{Host: "~/.secrets/token", Guest: "~/.secrets/token"},
},
},
}
vm := testVM("auth-missing", "image-auth-missing", "172.16.0.65")
vm.Runtime.WorkDiskPath = workDiskDir
if err := d.ensureOpencodeAuthOnWorkDisk(context.Background(), &vm); err != nil {
t.Fatalf("ensureOpencodeAuthOnWorkDisk: %v", err)
vm := testVM("sync-file", "image", "172.16.0.71")
vm.Runtime.WorkDiskPath = workDisk
if err := d.runFileSync(context.Background(), &vm); err != nil {
t.Fatalf("runFileSync: %v", err)
}
got, err := os.ReadFile(guestAuthPath)
dst := filepath.Join(workDisk, ".secrets", "token")
got, err := os.ReadFile(dst)
if err != nil {
t.Fatalf("ReadFile(guest auth): %v", err)
t.Fatal(err)
}
if string(got) != string(original) {
t.Fatalf("guest auth = %q, want preserved %q", string(got), string(original))
if string(got) != string(srcData) {
t.Fatalf("dst = %q, want %q", got, srcData)
}
entries := parseLogEntries(t, buf.Bytes())
if !hasLogEntry(entries, map[string]string{
"msg": "guest opencode auth sync skipped",
"vm_name": vm.Name,
"host_path": filepath.Join(homeDir, workDiskOpencodeAuthRelativePath),
}) {
t.Fatalf("expected warn log, got %v", entries)
}
}
func TestEnsureOpencodeAuthOnWorkDiskWarnsAndSkipsWhenHostAuthUnreadable(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
hostAuthPath := filepath.Join(homeDir, workDiskOpencodeAuthRelativePath)
if err := os.MkdirAll(hostAuthPath, 0o755); err != nil {
t.Fatalf("MkdirAll(host auth path as dir): %v", err)
}
workDiskDir := t.TempDir()
guestAuthPath := filepath.Join(workDiskDir, workDiskOpencodeAuthRelativePath)
if err := os.MkdirAll(filepath.Dir(guestAuthPath), 0o755); err != nil {
t.Fatalf("MkdirAll(guest auth dir): %v", err)
}
original := []byte("{\"token\":\"keep\"}\n")
if err := os.WriteFile(guestAuthPath, original, 0o600); err != nil {
t.Fatalf("WriteFile(guest auth): %v", err)
}
var buf bytes.Buffer
logger, _, err := newDaemonLogger(&buf, "info")
info, err := os.Stat(dst)
if err != nil {
t.Fatalf("newDaemonLogger: %v", err)
}
d := &Daemon{
runner: &filesystemRunner{t: t},
logger: logger,
}
vm := testVM("auth-unreadable", "image-auth-unreadable", "172.16.0.66")
vm.Runtime.WorkDiskPath = workDiskDir
if err := d.ensureOpencodeAuthOnWorkDisk(context.Background(), &vm); err != nil {
t.Fatalf("ensureOpencodeAuthOnWorkDisk: %v", err)
}
got, err := os.ReadFile(guestAuthPath)
if err != nil {
t.Fatalf("ReadFile(guest auth): %v", err)
}
if string(got) != string(original) {
t.Fatalf("guest auth = %q, want preserved %q", string(got), string(original))
}
entries := parseLogEntries(t, buf.Bytes())
if !hasLogEntry(entries, map[string]string{
"msg": "guest opencode auth sync skipped",
"vm_name": vm.Name,
"host_path": hostAuthPath,
"error": "is a directory",
}) {
t.Fatalf("expected warn log, got %v", entries)
}
}
func TestEnsureClaudeAuthOnWorkDiskCopiesHostAuth(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
hostAuthPath := filepath.Join(homeDir, workDiskClaudeAuthRelativePath)
if err := os.MkdirAll(filepath.Dir(hostAuthPath), 0o755); err != nil {
t.Fatalf("MkdirAll(host auth dir): %v", err)
}
hostAuth := []byte("{\"token\":\"claude\"}\n")
if err := os.WriteFile(hostAuthPath, hostAuth, 0o600); err != nil {
t.Fatalf("WriteFile(host auth): %v", err)
}
workDiskDir := t.TempDir()
d := &Daemon{runner: &filesystemRunner{t: t}}
vm := testVM("claude-auth", "image-claude-auth", "172.16.0.67")
vm.Runtime.WorkDiskPath = workDiskDir
if err := d.ensureClaudeAuthOnWorkDisk(context.Background(), &vm); err != nil {
t.Fatalf("ensureClaudeAuthOnWorkDisk: %v", err)
}
guestAuthPath := filepath.Join(workDiskDir, workDiskClaudeAuthRelativePath)
got, err := os.ReadFile(guestAuthPath)
if err != nil {
t.Fatalf("ReadFile(guest auth): %v", err)
}
if string(got) != string(hostAuth) {
t.Fatalf("guest auth = %q, want %q", string(got), string(hostAuth))
}
info, err := os.Stat(guestAuthPath)
if err != nil {
t.Fatalf("Stat(guest auth): %v", err)
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("guest auth mode = %v, want 0600", info.Mode().Perm())
t.Fatalf("mode = %v, want 0600", info.Mode().Perm())
}
}
func TestEnsurePiAuthOnWorkDiskCopiesHostAuth(t *testing.T) {
func TestRunFileSyncRespectsCustomMode(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
hostAuthPath := filepath.Join(homeDir, workDiskPiAuthRelativePath)
if err := os.MkdirAll(filepath.Dir(hostAuthPath), 0o755); err != nil {
t.Fatalf("MkdirAll(host auth dir): %v", err)
}
hostAuth := []byte("{\"token\":\"pi\"}\n")
if err := os.WriteFile(hostAuthPath, hostAuth, 0o600); err != nil {
t.Fatalf("WriteFile(host auth): %v", err)
srcPath := filepath.Join(homeDir, "script")
if err := os.WriteFile(srcPath, []byte("#!/bin/sh\nexit 0\n"), 0o600); err != nil {
t.Fatal(err)
}
workDiskDir := t.TempDir()
d := &Daemon{runner: &filesystemRunner{t: t}}
vm := testVM("pi-auth", "image-pi-auth", "172.16.0.68")
vm.Runtime.WorkDiskPath = workDiskDir
if err := d.ensurePiAuthOnWorkDisk(context.Background(), &vm); err != nil {
t.Fatalf("ensurePiAuthOnWorkDisk: %v", err)
workDisk := t.TempDir()
d := &Daemon{
runner: &filesystemRunner{t: t},
config: model.DaemonConfig{
FileSync: []model.FileSyncEntry{
{Host: "~/script", Guest: "~/bin/my-script", Mode: "0755"},
},
},
}
vm := testVM("sync-mode", "image", "172.16.0.72")
vm.Runtime.WorkDiskPath = workDisk
if err := d.runFileSync(context.Background(), &vm); err != nil {
t.Fatalf("runFileSync: %v", err)
}
guestAuthPath := filepath.Join(workDiskDir, workDiskPiAuthRelativePath)
got, err := os.ReadFile(guestAuthPath)
info, err := os.Stat(filepath.Join(workDisk, "bin", "my-script"))
if err != nil {
t.Fatalf("ReadFile(guest auth): %v", err)
t.Fatal(err)
}
if string(got) != string(hostAuth) {
t.Fatalf("guest auth = %q, want %q", string(got), string(hostAuth))
if info.Mode().Perm() != 0o755 {
t.Fatalf("mode = %v, want 0755", info.Mode().Perm())
}
}
func TestEnsurePiAuthOnWorkDiskWarnsAndSkipsWhenHostAuthMissing(t *testing.T) {
func TestRunFileSyncSkipsMissingHostPath(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
workDiskDir := t.TempDir()
guestAuthPath := filepath.Join(workDiskDir, workDiskPiAuthRelativePath)
if err := os.MkdirAll(filepath.Dir(guestAuthPath), 0o755); err != nil {
t.Fatalf("MkdirAll(guest auth dir): %v", err)
}
original := []byte("{\"token\":\"keep\"}\n")
if err := os.WriteFile(guestAuthPath, original, 0o600); err != nil {
t.Fatalf("WriteFile(guest auth): %v", err)
}
var buf bytes.Buffer
logger, _, err := newDaemonLogger(&buf, "info")
if err != nil {
t.Fatalf("newDaemonLogger: %v", err)
t.Fatal(err)
}
workDisk := t.TempDir()
d := &Daemon{
runner: &filesystemRunner{t: t},
logger: logger,
config: model.DaemonConfig{
FileSync: []model.FileSyncEntry{
{Host: "~/does-not-exist", Guest: "~/wherever"},
},
},
}
vm := testVM("pi-auth-missing", "image-pi-auth-missing", "172.16.0.69")
vm.Runtime.WorkDiskPath = workDiskDir
if err := d.ensurePiAuthOnWorkDisk(context.Background(), &vm); err != nil {
t.Fatalf("ensurePiAuthOnWorkDisk: %v", err)
}
got, err := os.ReadFile(guestAuthPath)
if err != nil {
t.Fatalf("ReadFile(guest auth): %v", err)
}
if string(got) != string(original) {
t.Fatalf("guest auth = %q, want preserved %q", string(got), string(original))
vm := testVM("sync-missing", "image", "172.16.0.73")
vm.Runtime.WorkDiskPath = workDisk
if err := d.runFileSync(context.Background(), &vm); err != nil {
t.Fatalf("runFileSync: %v", err)
}
entries := parseLogEntries(t, buf.Bytes())
if !hasLogEntry(entries, map[string]string{
"msg": "guest pi auth sync skipped",
"msg": "file_sync skipped",
"vm_name": vm.Name,
"host_path": filepath.Join(homeDir, workDiskPiAuthRelativePath),
"host_path": filepath.Join(homeDir, "does-not-exist"),
}) {
t.Fatalf("expected warn log, got %v", entries)
t.Fatalf("expected skipped log, got %v", entries)
}
}
func TestRunFileSyncOverwritesExistingGuestFile(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
srcPath := filepath.Join(homeDir, "token")
if err := os.WriteFile(srcPath, []byte("fresh"), 0o600); err != nil {
t.Fatal(err)
}
workDisk := t.TempDir()
// Work disk is mounted at /root in the guest, so the guest path
// "/root/token" maps to workDisk/token here.
existing := filepath.Join(workDisk, "token")
if err := os.WriteFile(existing, []byte("stale"), 0o600); err != nil {
t.Fatal(err)
}
d := &Daemon{
runner: &filesystemRunner{t: t},
config: model.DaemonConfig{
FileSync: []model.FileSyncEntry{
{Host: "~/token", Guest: "/root/token"},
},
},
}
vm := testVM("sync-overwrite", "image", "172.16.0.74")
vm.Runtime.WorkDiskPath = workDisk
if err := d.runFileSync(context.Background(), &vm); err != nil {
t.Fatalf("runFileSync: %v", err)
}
got, err := os.ReadFile(existing)
if err != nil {
t.Fatal(err)
}
if string(got) != "fresh" {
t.Fatalf("guest file = %q, want fresh", got)
}
}
func TestRunFileSyncCopiesDirectoryRecursively(t *testing.T) {
homeDir := t.TempDir()
t.Setenv("HOME", homeDir)
srcDir := filepath.Join(homeDir, ".aws")
if err := os.MkdirAll(srcDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(srcDir, "credentials"), []byte("access"), 0o600); err != nil {
t.Fatal(err)
}
sub := filepath.Join(srcDir, "sso", "cache")
if err := os.MkdirAll(sub, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sub, "token.json"), []byte("sso-token"), 0o600); err != nil {
t.Fatal(err)
}
workDisk := t.TempDir()
d := &Daemon{
runner: &filesystemRunner{t: t},
config: model.DaemonConfig{
FileSync: []model.FileSyncEntry{
{Host: "~/.aws", Guest: "~/.aws"},
},
},
}
vm := testVM("sync-dir", "image", "172.16.0.75")
vm.Runtime.WorkDiskPath = workDisk
if err := d.runFileSync(context.Background(), &vm); err != nil {
t.Fatalf("runFileSync: %v", err)
}
creds, err := os.ReadFile(filepath.Join(workDisk, ".aws", "credentials"))
if err != nil {
t.Fatal(err)
}
if string(creds) != "access" {
t.Fatalf("credentials = %q, want access", creds)
}
ssoToken, err := os.ReadFile(filepath.Join(workDisk, ".aws", "sso", "cache", "token.json"))
if err != nil {
t.Fatal(err)
}
if string(ssoToken) != "sso-token" {
t.Fatalf("sso token = %q, want sso-token", ssoToken)
}
}
@ -1931,26 +1841,61 @@ func (r *filesystemRunner) RunSudo(ctx context.Context, args ...string) ([]byte,
}
return os.ReadFile(args[1])
case "install":
if len(args) != 5 || args[1] != "-m" {
return nil, fmt.Errorf("unexpected install args: %v", args)
}
mode, err := strconv.ParseUint(args[2], 8, 32)
// Minimal install(1): expected forms are
// install -m MODE SRC DST (5 args)
// install -o 0 -g 0 -m MODE SRC DST (9 args, ignored owners)
src, dst, mode, err := parseInstallArgs(args)
if err != nil {
return nil, err
}
data, err := os.ReadFile(args[3])
data, err := os.ReadFile(src)
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(args[4]), 0o755); err != nil {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return nil, err
}
return nil, os.WriteFile(args[4], data, os.FileMode(mode))
return nil, os.WriteFile(dst, data, os.FileMode(mode))
case "chown":
// chown -R OWNER TARGET — owner is ignored under test; we
// already run as the test user and os.Chown would require
// CAP_CHOWN.
if len(args) != 4 || args[1] != "-R" {
return nil, fmt.Errorf("unexpected chown args: %v", args)
}
return nil, nil
default:
return nil, fmt.Errorf("unexpected sudo command: %v", args)
}
}
// parseInstallArgs recognises the `install` invocations banger emits
// and returns (source, destination, parsed mode). Anything else is an
// error so the test stub stays a closed set.
func parseInstallArgs(args []string) (string, string, os.FileMode, error) {
switch len(args) {
case 5:
if args[1] != "-m" {
return "", "", 0, fmt.Errorf("unexpected install args: %v", args)
}
mode, err := strconv.ParseUint(args[2], 8, 32)
if err != nil {
return "", "", 0, err
}
return args[3], args[4], os.FileMode(mode), nil
case 9:
if args[1] != "-o" || args[3] != "-g" || args[5] != "-m" {
return "", "", 0, fmt.Errorf("unexpected install args: %v", args)
}
mode, err := strconv.ParseUint(args[6], 8, 32)
if err != nil {
return "", "", 0, err
}
return args[7], args[8], os.FileMode(mode), nil
}
return "", "", 0, fmt.Errorf("unexpected install args: %v", args)
}
func copyIntoDir(sourcePath, targetDir string) error {
targetDir = strings.TrimSuffix(targetDir, "/")
info, err := os.Stat(sourcePath)