Skip to content

Commit 7c67883

Browse files
authored
Opt-in host dotfiles snapshot sync. (#1)
1 parent 36372f6 commit 7c67883

7 files changed

Lines changed: 158 additions & 7 deletions

File tree

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,25 @@ DVM_DISK="80GiB"
7979

8080
DVM_PACKAGES="git openssh-clients gpg helix ripgrep fd-find jq"
8181
DVM_SETUP_SCRIPTS="$DVM_CONFIG/setup.d/fedora.sh"
82+
DVM_DOTFILES_DIR="$HOME/.dotfiles"
8283
```
8384

84-
Put package-independent setup, public dotfiles, shell config, and tool config in
85-
`setup.d/fedora.sh`. User setup scripts run inside the VM as the guest user with:
85+
Put package-independent setup, shell config, and tool config in `setup.d/fedora.sh`.
86+
If `DVM_DOTFILES_DIR` is set, DVM copies a snapshot of that host directory into the VM
87+
before user setup scripts run. It does not mount the host directory live. User setup
88+
scripts run inside the VM as the guest user with:
8689

8790
```text
8891
DVM_NAME
8992
DVM_VM_NAME
9093
DVM_CODE_DIR
94+
DVM_DOTFILES_TARGET
9195
```
9296

97+
Dotfiles sync is opt-in. By default DVM excludes `.git`, `.ssh`, `.gnupg`, `.env`, and
98+
`secrets`, refuses dangerous source paths such as `/`, `$HOME`, `~/.ssh`, and
99+
`~/.gnupg`, and keeps the target under the guest home directory.
100+
93101
The default workflow keeps source code inside the VM under `~/code`. No host project
94102
directory is mounted.
95103

@@ -137,8 +145,9 @@ dvm setup myapp
137145
dvm setup-all
138146
```
139147

140-
This is the intended way to add packages everywhere or refresh dotfiles. The script
141-
does not try to remove packages automatically; removals should be explicit and manual.
148+
This is the intended way to add packages everywhere or refresh dotfiles snapshots. The
149+
script does not try to remove packages automatically; removals should be explicit and
150+
manual.
142151

143152
## Delete Safety
144153

SECURITY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ User setup scripts, dotfiles, packages installed inside a VM, downloaded models,
3939
services configured by users are user-controlled and out of scope unless DVM itself
4040
handles them insecurely.
4141

42+
DVM does not mount host dotfiles into VMs by default. If dotfiles sync is enabled, DVM
43+
copies a filtered snapshot during setup so project code in the VM does not retain a
44+
persistent read path back to the host.
45+
4246
## Safe Installation
4347

4448
Install from a signed release tag, not from an arbitrary branch. Before running

defaults/config.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,12 @@ DVM_PACKAGES="${DVM_PACKAGES:-git openssh-clients gpg}"
2121
# guest user with DVM_NAME, DVM_VM_NAME, and DVM_CODE_DIR set.
2222
DVM_SETUP_SCRIPTS="${DVM_SETUP_SCRIPTS:-$DVM_CONFIG/setup.d/fedora.sh}"
2323

24+
# Optional host dotfiles snapshot copied into the VM during `dvm setup`.
25+
# Keep this opt-in. DVM copies a snapshot before user setup scripts run; it does
26+
# not mount a live host directory into the VM.
27+
# DVM_DOTFILES_DIR="${HOME}/.dotfiles"
28+
DVM_DOTFILES_DIR="${DVM_DOTFILES_DIR:-}"
29+
DVM_DOTFILES_TARGET="${DVM_DOTFILES_TARGET:-$DVM_GUEST_HOME/.dotfiles}"
30+
DVM_DOTFILES_EXCLUDES="${DVM_DOTFILES_EXCLUDES:-.git .ssh .gnupg .env secrets}"
31+
2432
DVM_GPG_DIR="${DVM_GPG_DIR:-$DVM_STATE/gpg}"

defaults/setup-fedora.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ mkdir -p "$DVM_CODE_DIR"
1212
# sudo dnf5 install -y helix ripgrep fd-find jq
1313
# fi
1414
#
15-
# if [ ! -d "$HOME/.dotfiles" ]; then
16-
# git clone https://github.com/example/dotfiles.git "$HOME/.dotfiles"
15+
# if [ -x "$DVM_DOTFILES_TARGET/install.sh" ]; then
16+
# "$DVM_DOTFILES_TARGET/install.sh"
1717
# fi
18-
# "$HOME/.dotfiles/install.sh"

lib/config.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ dvm_load_config() {
2121
DVM_CODE_DIR="${DVM_CODE_DIR:-$DVM_GUEST_HOME/code}"
2222
DVM_PACKAGES="${DVM_PACKAGES:-git openssh-clients gpg}"
2323
DVM_SETUP_SCRIPTS="${DVM_SETUP_SCRIPTS:-$DVM_CONFIG/setup.d/fedora.sh}"
24+
DVM_DOTFILES_DIR="${DVM_DOTFILES_DIR:-}"
25+
DVM_DOTFILES_TARGET="${DVM_DOTFILES_TARGET:-$DVM_GUEST_HOME/.dotfiles}"
26+
DVM_DOTFILES_EXCLUDES="${DVM_DOTFILES_EXCLUDES:-.git .ssh .gnupg .env secrets}"
2427
DVM_GPG_DIR="${DVM_GPG_DIR:-$DVM_STATE/gpg}"
2528
}
2629

lib/vm.sh

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,90 @@ fi
110110
REMOTE
111111
}
112112

113+
dvm_resolve_host_dir() {
114+
local dir
115+
dir="$1"
116+
(
117+
cd "$dir" 2>/dev/null &&
118+
pwd -P
119+
) || dvm_die "directory not found: $dir"
120+
}
121+
122+
dvm_validate_dotfiles_source() {
123+
local source_real home_real
124+
source_real="$1"
125+
home_real="$(dvm_resolve_host_dir "$HOME")"
126+
127+
case "$source_real" in
128+
/ | "$home_real" | "$home_real/.ssh" | "$home_real/.ssh/"* | "$home_real/.gnupg" | "$home_real/.gnupg/"*)
129+
dvm_die "refusing dangerous DVM_DOTFILES_DIR: $source_real"
130+
;;
131+
esac
132+
}
133+
134+
dvm_validate_dotfiles_target() {
135+
local target
136+
target="$1"
137+
138+
case "$target" in
139+
/*) ;;
140+
*) dvm_die "DVM_DOTFILES_TARGET must be an absolute path: $target" ;;
141+
esac
142+
143+
case "$target" in
144+
"$DVM_GUEST_HOME")
145+
dvm_die "refusing unsafe DVM_DOTFILES_TARGET: $target"
146+
;;
147+
"$DVM_GUEST_HOME/.ssh" | "$DVM_GUEST_HOME/.ssh/"* | \
148+
"$DVM_GUEST_HOME/.gnupg" | "$DVM_GUEST_HOME/.gnupg/"*)
149+
dvm_die "refusing unsafe DVM_DOTFILES_TARGET: $target"
150+
;;
151+
"$DVM_GUEST_HOME"/*) ;;
152+
*) dvm_die "DVM_DOTFILES_TARGET must stay under DVM_GUEST_HOME: $target" ;;
153+
esac
154+
}
155+
156+
dvm_sync_dotfiles_remote() {
157+
cat <<'REMOTE'
158+
set -euo pipefail
159+
target="$1"
160+
parent="$(dirname "$target")"
161+
162+
mkdir -p "$parent"
163+
rm -rf "$target"
164+
mkdir -p "$target"
165+
tar -C "$target" -xf -
166+
REMOTE
167+
}
168+
169+
dvm_sync_dotfiles() {
170+
local vm source_real target remote exclude
171+
vm="$1"
172+
[ -n "$DVM_DOTFILES_DIR" ] || return 0
173+
174+
[ -d "$DVM_DOTFILES_DIR" ] || dvm_die "dotfiles directory not found: $DVM_DOTFILES_DIR"
175+
dvm_require tar
176+
177+
source_real="$(dvm_resolve_host_dir "$DVM_DOTFILES_DIR")"
178+
dvm_validate_dotfiles_source "$source_real"
179+
180+
target="$DVM_DOTFILES_TARGET"
181+
dvm_validate_dotfiles_target "$target"
182+
183+
remote="$(dvm_sync_dotfiles_remote)"
184+
185+
dvm_log "syncing dotfiles into $vm: $source_real -> $target"
186+
(
187+
cd "$source_real" || exit 1
188+
set -- tar -cf -
189+
for exclude in $DVM_DOTFILES_EXCLUDES; do
190+
set -- "$@" --exclude "$exclude"
191+
done
192+
set -- "$@" .
193+
"$@"
194+
) | limactl shell "$vm" bash -c "$remote" dvm-dotfiles "$target"
195+
}
196+
113197
dvm_setup() {
114198
local name vm remote script
115199
[ "$#" -eq 1 ] || dvm_die "usage: dvm setup <name>"
@@ -123,6 +207,7 @@ dvm_setup() {
123207
limactl start "$vm"
124208
dvm_log "running core setup in $vm"
125209
limactl shell "$vm" bash -c "$remote" dvm-setup "$name" "$DVM_CODE_DIR" "$DVM_PACKAGES"
210+
dvm_sync_dotfiles "$vm"
126211

127212
for script in $DVM_SETUP_SCRIPTS; do
128213
[ -n "$script" ] || continue
@@ -132,6 +217,7 @@ dvm_setup() {
132217
"DVM_NAME=$name" \
133218
"DVM_VM_NAME=$vm" \
134219
"DVM_CODE_DIR=$DVM_CODE_DIR" \
220+
"DVM_DOTFILES_TARGET=$DVM_DOTFILES_TARGET" \
135221
bash -s <"$script"
136222
done
137223
}

tests/smoke.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,16 @@ export PATH="$MOCK_BIN:$PATH"
118118
export HOME="$TMP/home"
119119
export DVM_CONFIG="$TMP/config"
120120
export DVM_STATE="$TMP/state"
121+
export HOST_DOTFILES="$TMP/host-dotfiles"
121122
mkdir -p "$HOME"
123+
mkdir -p "$HOST_DOTFILES/.git" "$HOST_DOTFILES/.ssh" "$HOST_DOTFILES/.gnupg"
124+
printf 'set -o vi\n' >"$HOST_DOTFILES/bashrc"
125+
printf '#!/usr/bin/env bash\n' >"$HOST_DOTFILES/install.sh"
126+
printf 'git metadata\n' >"$HOST_DOTFILES/.git/config"
127+
printf 'secret key\n' >"$HOST_DOTFILES/.ssh/id_ed25519"
128+
printf 'gpg material\n' >"$HOST_DOTFILES/.gnupg/private-keys-v1.d"
129+
printf 'token=1\n' >"$HOST_DOTFILES/.env"
130+
printf 'do not copy\n' >"$HOST_DOTFILES/secrets"
122131

123132
"$ROOT/install.sh" --prefix "$TMP/local-bin" --name dvm-test --init >/dev/null
124133
[ -L "$TMP/local-bin/dvm-test" ]
@@ -127,33 +136,66 @@ mkdir -p "$HOME"
127136

128137
cat >"$DVM_CONFIG/config.sh" <<CONFIG
129138
DVM_PREFIX="testvm"
139+
DVM_GUEST_HOME="$VM_HOME_ROOT/testvm-app"
130140
DVM_CODE_DIR="$VM_HOME_ROOT/testvm-app/code"
131141
DVM_PACKAGES="git openssh-clients gpg helix"
132142
DVM_SETUP_SCRIPTS="$DVM_CONFIG/setup.d/fedora.sh"
143+
DVM_DOTFILES_DIR="$HOST_DOTFILES"
144+
DVM_DOTFILES_TARGET="$VM_HOME_ROOT/testvm-app/.dotfiles"
133145
DVM_GPG_DIR="$DVM_STATE/gpg"
134146
CONFIG
135147

136148
cat >"$DVM_CONFIG/setup.d/fedora.sh" <<'SCRIPT'
137149
#!/usr/bin/env bash
138150
set -euo pipefail
139151
printf '%s\n' "$DVM_NAME" >>"$HOME/setup-ran"
152+
[ -f "$DVM_DOTFILES_TARGET/install.sh" ]
140153
SCRIPT
141154

142155
"$TMP/local-bin/dvm-test" new app >"$TMP/new.out"
143156
grep -Fq 'public key for app' "$TMP/new.out"
144157
grep -Fq 'create testvm-app' "$LOG"
145158
grep -Fq 'helix' "$LOG"
146159
grep -Fq 'app' "$VM_HOME_ROOT/testvm-app/setup-ran"
160+
[ -f "$VM_HOME_ROOT/testvm-app/.dotfiles/bashrc" ]
161+
[ -f "$VM_HOME_ROOT/testvm-app/.dotfiles/install.sh" ]
162+
[ ! -e "$VM_HOME_ROOT/testvm-app/.dotfiles/.git" ]
163+
[ ! -e "$VM_HOME_ROOT/testvm-app/.dotfiles/.ssh" ]
164+
[ ! -e "$VM_HOME_ROOT/testvm-app/.dotfiles/.gnupg" ]
165+
[ ! -e "$VM_HOME_ROOT/testvm-app/.dotfiles/.env" ]
166+
[ ! -e "$VM_HOME_ROOT/testvm-app/.dotfiles/secrets" ]
147167

148168
"$TMP/local-bin/dvm-test" list >"$TMP/list.out"
149169
grep -Fxq app "$TMP/list.out"
170+
rm -f "$HOST_DOTFILES/bashrc"
171+
printf 'export EDITOR=hx\n' >"$HOST_DOTFILES/zshrc"
150172
"$TMP/local-bin/dvm-test" setup-all >/dev/null
151173
[ "$(grep -Fc 'app' "$VM_HOME_ROOT/testvm-app/setup-ran")" -ge 2 ]
174+
[ ! -e "$VM_HOME_ROOT/testvm-app/.dotfiles/bashrc" ]
175+
[ -f "$VM_HOME_ROOT/testvm-app/.dotfiles/zshrc" ]
152176
"$TMP/local-bin/dvm-test" key app >"$TMP/key.out"
153177
grep -Fq 'ssh-ed25519' "$TMP/key.out"
154178
"$TMP/local-bin/dvm-test" doctor >"$TMP/doctor.out"
155179
grep -Fq "prefix: testvm" "$TMP/doctor.out"
156180

181+
cp "$DVM_CONFIG/config.sh" "$DVM_CONFIG/config.safe.sh"
182+
cat >"$DVM_CONFIG/config.sh" <<CONFIG
183+
DVM_PREFIX="testvm"
184+
DVM_GUEST_HOME="$VM_HOME_ROOT/testvm-app"
185+
DVM_CODE_DIR="$VM_HOME_ROOT/testvm-app/code"
186+
DVM_PACKAGES="git openssh-clients gpg helix"
187+
DVM_SETUP_SCRIPTS="$DVM_CONFIG/setup.d/fedora.sh"
188+
DVM_DOTFILES_DIR="$HOME"
189+
DVM_DOTFILES_TARGET="$VM_HOME_ROOT/testvm-app/.dotfiles"
190+
DVM_GPG_DIR="$DVM_STATE/gpg"
191+
CONFIG
192+
if "$TMP/local-bin/dvm-test" setup app >"$TMP/dangerous.out" 2>"$TMP/dangerous.err"; then
193+
echo "setup unexpectedly succeeded with dangerous dotfiles dir" >&2
194+
exit 1
195+
fi
196+
grep -Fq 'refusing dangerous DVM_DOTFILES_DIR' "$TMP/dangerous.err"
197+
mv "$DVM_CONFIG/config.safe.sh" "$DVM_CONFIG/config.sh"
198+
157199
mkdir -p "$VM_HOME_ROOT/testvm-app/code/repo"
158200
git -C "$VM_HOME_ROOT/testvm-app/code/repo" init -q
159201
printf 'dirty\n' >"$VM_HOME_ROOT/testvm-app/code/repo/file.txt"

0 commit comments

Comments
 (0)