Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ stable v1.2 release and includes that fix.
SSH server Feature, so `gh codespace ssh` can attach as documented.
- Codespaces reconcile their post-create default Selections without opening
interactive pickers when the orchestrator assigns a pseudo-TTY.
- Docker Boxes can remap `dev` to a non-default PUID/PGID even when lifecycle
adapters mount managed files read-only beneath the Managed home.
- Docker Boxes can remap `dev` to a non-default PUID/PGID even with read-only
managed files beneath the Managed home. Native unprivileged Linux lifecycle
installs now require those IDs to match the invoking account; intentional
NAS/Unraid remapping remains available to root-run rootful runtimes.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Flags: `--build` (build from source), `--edge` (latest `main`), `--adopt`
| `SQUAREBOX_TAG` | latest published stable | Published Release to install (for example `v1.2.1`). Tags use `vMAJOR.MINOR.PATCH[-prerelease]`; build metadata is not published. |
| `SQUAREBOX_IMAGE` | value from `release.json` | Optional image-repository override for development/testing. |
| `SQUAREBOX_BUILD` | `0` | `1` is equivalent to `--build`. |
| `PUID` / `PGID` | invoking Linux user | Host uid/gid that should own bind-mounted files. Docker/rootful hosts may override these (Unraid/NAS: `99` / `100`); rootless Podman requires the invoking host identity. |
| `PUID` / `PGID` | invoking Linux user | Host uid/gid that should own bind-mounted files. Unprivileged Linux installs must match the invoking account. Root-run rootful runtimes, including Docker or Podman on NAS and Unraid, may override these (typically `99` / `100`); rootless Podman also requires the invoking host identity. |
| `SQUAREBOX_RUNTIME` | auto | Force `docker` or `podman`. |
| `SQUAREBOX_HOME_VOLUME` | `squarebox-home` | Name of the named volume backing `/home/dev`. |
| `SQUAREBOX_EDGE` | `0` | `1` is equivalent to `--edge`. |
Expand Down
63 changes: 59 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ than a hostile-code sandbox. Host DAC permissions and the declared mounts
remain the access boundary.
Because the entrypoint is already unprivileged in this mode, rootless Podman
rejects PUID/PGID overrides that differ from the invoking host identity;
Docker and rootful runtimes retain their remapping behavior.
the native Linux POSIX lifecycle adapter rejects the same mismatch for every
unprivileged runtime before it mutates host state. Root-run rootful runtimes
retain explicit remapping for administered NAS and Unraid installations.

The installer does not install host packages. On legacy Linux installs it may
need to repair ownership that an older Squarebox release changed; any host
Expand Down Expand Up @@ -241,9 +243,62 @@ SSH files as a fallback—or as native PowerShell's current SSH path—exposes t
contents read-only to Box processes.

The entrypoint validates numeric UID/GID inputs and refuses unsafe Managed-home
dotfile symlinks. Rootless Podman maps keep-id to the image's `dev` account and
leaves host SELinux labels untouched by disabling label separation for the Box;
validate this tradeoff on the target host before using sensitive credentials.
dotfile symlinks. On native Linux, an unprivileged lifecycle install also
requires PUID/PGID to match the invoking account because a different Box owner
cannot safely share its private host-bound lifecycle files. Explicit ownership
remapping is reserved for root-run rootful runtimes, including Docker or Podman
on NAS and Unraid.
Rootless Podman maps keep-id to the image's `dev` account and leaves host SELinux
labels untouched by disabling label separation for the Box; validate this
tradeoff on the target host before using sensitive credentials.

If an existing POSIX Install identity records IDs that no longer match the
invoking Linux account, adopt the current account without changing channels or
paths:

```bash
PUID="$(id -u)" PGID="$(id -g)" "$SQUAREBOX_DIR/install.sh"
```

An older Box may already have changed the specific host binds that it mounted
beneath `/home/dev`. Stop the recorded Box, then restore only those paths after
validating the recorded install and Workspace identities:

```bash
set -euo pipefail
case "${SQUAREBOX_DIR:-}" in /*) ;; *) exit 64 ;; esac
[ "$SQUAREBOX_DIR" != / ] && [ ! -L "$SQUAREBOX_DIR" ] || exit 64
state="$SQUAREBOX_DIR/.squarebox/install-state"
sudo test -f "$state" && sudo test ! -L "$state" || exit 1
state_value() {
sudo awk -F= -v wanted="$1" '
$1 == wanted { count++; value = substr($0, index($0, "=") + 1) }
END { if (count != 1) exit 1; print value }
' "$state"
}
format=$(state_value FORMAT)
recorded_install=$(state_value INSTALL_DIR)
workspace=$(state_value WORKSPACE_DIR)
[ "$format" = 1 ] && [ "$recorded_install" = "$SQUAREBOX_DIR" ] || exit 64
case "$workspace" in /*) ;; *) exit 64 ;; esac
[ "$workspace" != / ] && sudo test -d "$workspace" \
&& sudo test ! -L "$workspace" || exit 64

git_identity="$SQUAREBOX_DIR/.squarebox/identity/git"
lazygit_config="$SQUAREBOX_DIR/.config/lazygit"
starship_config="$SQUAREBOX_DIR/.config/starship.toml"
sudo test -d "$git_identity" && sudo test ! -L "$git_identity" || exit 1
sudo test -d "$lazygit_config" && sudo test ! -L "$lazygit_config" || exit 1
sudo test -f "$starship_config" && sudo test ! -L "$starship_config" || exit 1

owner="$(id -u):$(id -g)"
sudo chown -R --preserve-root -- "$owner" "$git_identity" "$lazygit_config"
sudo chown --no-dereference -- "$owner" "$starship_config" "$workspace"
PUID="$(id -u)" PGID="$(id -g)" "$SQUAREBOX_DIR/install.sh"
```

Do not replace these resolved paths with the home directory, the install root,
or another broad recursive target.

## Safe lifecycle deletion

Expand Down
6 changes: 4 additions & 2 deletions docs/releases/v1.2.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ same Release.
defaults can select terminal multiplexers. Codespaces also include a pinned
SSH server Feature so `gh codespace ssh` works with the custom image, and
post-create reconciliation remains noninteractive even when given a PTY.
- Docker PUID/PGID remapping remains compatible with read-only managed files
mounted beneath `/home/dev`, including the lifecycle-managed Bash profile.
- Image-level Docker PUID/PGID remapping remains compatible with read-only
managed files beneath `/home/dev`. For the POSIX lifecycle adapter, a native
unprivileged Linux install must use the invoking account IDs; intentional
NAS/Unraid remapping requires a root-run rootful runtime.
- xh, just, and mise advance with reviewed amd64 and arm64 checksums.

The new optional tools are not forced into an existing Selection. Run
Expand Down
12 changes: 10 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,10 @@ if [ "$HAD_STATE" = 1 ] && [ "$HOME_VOLUME" != "$STATE_HOME_VOLUME" ]; then
echo "Error: cannot change the recorded Managed-home name during rebuild; uninstall this identity first." >&2; exit 1
fi

if [ "$(uname -s)" = Linux ]; then
_host_uid="$(id -u)"; _host_gid="$(id -g)"
_host_os="$(uname -s)"
_invoking_uid="$(id -u)"; _invoking_gid="$(id -g)"
if [ "$_host_os" = Linux ]; then
_host_uid="$_invoking_uid"; _host_gid="$_invoking_gid"
# The image cannot safely remap dev to uid/gid zero.
[ "$_host_uid" -gt 0 ] || _host_uid=1000
[ "$_host_gid" -gt 0 ] || _host_gid=1000
Expand All @@ -255,6 +257,12 @@ validate_id() {
}
validate_id PUID "$PUID"; validate_id PGID "$PGID"
PUID="$((10#$PUID))"; PGID="$((10#$PGID))"
if [ "$_host_os" = Linux ] && [ "$_invoking_uid" -ne 0 ] \
&& { [ "$PUID" != "$_invoking_uid" ] || [ "$PGID" != "$_invoking_gid" ]; }; then
echo "Error: an unprivileged Linux install requires PUID/PGID to match the invoking host identity ($_invoking_uid:$_invoking_gid); requested $PUID:$PGID." >&2
echo " Run the installer as the target account. Only root-run rootful runtimes, including Docker or Podman on NAS and Unraid, may remap ownership." >&2
exit 64
fi
case "$EDGE:$BUILD" in 0:0|0:1|1:1) ;; *) echo "Error: EDGE/BUILD must be 0 or 1." >&2; exit 64 ;; esac
for _path in "$INSTALL_DIR" "$WORKSPACE_DIR" "$GIT_CONFIG_DIR"; do
is_absolute_state_path "$_path" || { echo "Error: lifecycle paths must be absolute and normalized (got '$_path')." >&2; exit 64; }
Expand Down
124 changes: 118 additions & 6 deletions tests/test-lifecycle-install-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,71 @@ cat >"$TMP/bin/winpty" <<'EOF'
printf '%s|%s|%s\n' "${MSYS_NO_PATHCONV:-}" "${MSYS2_ARG_CONV_EXCL:-}" "$*" >>"$MOCK_RUNTIME/winpty-calls"
exec "$@"
EOF
cat >"$TMP/bin/id" <<'EOF'
#!/usr/bin/env bash
case "${1:-}" in
-u) [ -z "${MOCK_ID_UID:-}" ] || { printf '%s\n' "$MOCK_ID_UID"; exit 0; } ;;
-g) [ -z "${MOCK_ID_GID:-}" ] || { printf '%s\n' "$MOCK_ID_GID"; exit 0; } ;;
esac
exec /usr/bin/id "$@"
EOF
cat >"$TMP/bin/uname" <<'EOF'
#!/usr/bin/env bash
if [ "${1:-}" = -s ] && [ -n "${MOCK_UNAME_S:-}" ]; then
printf '%s\n' "$MOCK_UNAME_S"
exit 0
fi
exec /usr/bin/uname "$@"
EOF
ln -s docker "$TMP/bin/podman"
chmod +x "$TMP/bin/"*

INSTALL="$TMP/custom squarebox=managed"
export PATH="$TMP/bin:$PATH" HOME="$TMP/home" SHELL=/bin/bash
PRIMARY_HOME="$HOME"

# A native unprivileged Linux adapter cannot safely manage host-owned private
# config after asking the Box to use a different numeric identity. Reject that
# split before the checkout or any runtime resource exists.
HOST_UID=12345; HOST_GID=23456
BAD_UID=12346; BAD_GID=23457
mismatch_index=0
for requested_identity in "$BAD_UID:$HOST_GID" "$HOST_UID:$BAD_GID"; do
mismatch_index=$((mismatch_index + 1))
requested_uid=${requested_identity%%:*}
requested_gid=${requested_identity#*:}
MISMATCH_RUNTIME="$TMP/runtime-puid-mismatch-$mismatch_index"
MISMATCH_INSTALL="$TMP/puid-mismatch-$mismatch_index"
mkdir -p "$MISMATCH_RUNTIME"
set +e
HOME="$TMP/home" MOCK_RUNTIME="$MISMATCH_RUNTIME" \
MOCK_UNAME_S=Linux MOCK_ID_UID="$HOST_UID" MOCK_ID_GID="$HOST_GID" \
SQUAREBOX_DIR="$MISMATCH_INSTALL" SQUAREBOX_RUNTIME=docker SQUAREBOX_TAG=v1.1.0 \
PUID="$requested_uid" PGID="$requested_gid" \
"$ROOT/install.sh" </dev/null >"$TMP/puid-mismatch-$mismatch_index.out" 2>&1
mismatch_rc=$?
set -e
if [ "$mismatch_rc" -ne 64 ]; then
echo "installer returned $mismatch_rc for an unsafe unprivileged PUID/PGID mismatch; expected 64" >&2
cat "$TMP/puid-mismatch-$mismatch_index.out" >&2
exit 1
fi
grep -q "requires PUID/PGID to match the invoking host identity ($HOST_UID:$HOST_GID)" \
"$TMP/puid-mismatch-$mismatch_index.out"
grep -q "requested $requested_uid:$requested_gid" "$TMP/puid-mismatch-$mismatch_index.out"
test ! -e "$MISMATCH_INSTALL"
test ! -e "$MISMATCH_RUNTIME/calls"
done

MATCH_RUNTIME="$TMP/runtime-puid-match"
mkdir -p "$MATCH_RUNTIME" "$TMP/puid-match-home"
HOME="$TMP/puid-match-home" MOCK_RUNTIME="$MATCH_RUNTIME" \
MOCK_UNAME_S=Linux MOCK_ID_UID="$HOST_UID" MOCK_ID_GID="$HOST_GID" \
SQUAREBOX_DIR="$TMP/puid-match" SQUAREBOX_RUNTIME=docker SQUAREBOX_TAG=v1.1.0 \
PUID="$HOST_UID" PGID="$HOST_GID" "$ROOT/install.sh" </dev/null
grep -qxF "PUID=$HOST_UID" "$TMP/puid-match/.squarebox/install-state"
grep -qxF "PGID=$HOST_GID" "$TMP/puid-match/.squarebox/install-state"

if SQUAREBOX_DIR="$TMP/unsafe" SQUAREBOX_WORKSPACE=/ SQUAREBOX_RUNTIME=docker SQUAREBOX_TAG=v1.1.0 \
"$ROOT/install.sh" </dev/null >"$TMP/unsafe-workspace.out" 2>&1; then
echo 'installer accepted a root Workspace before recording state' >&2
Expand All @@ -189,9 +248,12 @@ GIT_BASH_RUNTIME="$TMP/runtime-git-bash"
mkdir -p "$GIT_BASH_HOME" "$GIT_BASH_RUNTIME"
(cd "$TMP" && HOME="$GIT_BASH_HOME" MSYSTEM=MINGW64 USERPROFILE='C:/Users/Test' \
SQUAREBOX_DIR="$GIT_BASH_INSTALL" SQUAREBOX_RUNTIME=docker SQUAREBOX_TAG=v1.1.0 \
MOCK_RUNTIME="$GIT_BASH_RUNTIME" "$ROOT/install.sh" </dev/null)
MOCK_RUNTIME="$GIT_BASH_RUNTIME" MOCK_UNAME_S=MINGW64_NT-10.0 \
PUID=4242 PGID=4243 "$ROOT/install.sh" </dev/null)
GIT_BASH_STATE="$TMP/$GIT_BASH_INSTALL/.squarebox/install-state"
grep -qxF "INSTALL_DIR=$GIT_BASH_INSTALL" "$GIT_BASH_STATE"
grep -qxF 'PUID=4242' "$GIT_BASH_STATE"
grep -qxF 'PGID=4243' "$GIT_BASH_STATE"
(cd "$TMP" && HOME="$GIT_BASH_HOME" MSYSTEM=MINGW64 USERPROFILE='C:/Users/Test' \
SQUAREBOX_DIR="$GIT_BASH_INSTALL" MOCK_RUNTIME="$GIT_BASH_RUNTIME" \
"$ROOT/uninstall.sh" --yes >/dev/null)
Expand All @@ -204,6 +266,26 @@ if (cd "$TMP" && env -u MSYSTEM -u USERPROFILE HOME="$GIT_BASH_HOME" \
fi
grep -q 'invalid Install identity' "$TMP/linux-uninstall-drive.out"

# Root-run rootful runtimes may intentionally assign NAS/Unraid ownership, and
# non-Linux Docker Desktop adapters retain their adapter-native Box identity.
ROOTFUL_RUNTIME="$TMP/runtime-rootful-override"
mkdir -p "$ROOTFUL_RUNTIME" "$TMP/rootful-home"
HOME="$TMP/rootful-home" MOCK_RUNTIME="$ROOTFUL_RUNTIME" \
MOCK_UNAME_S=Linux MOCK_ID_UID=0 MOCK_ID_GID=0 \
SQUAREBOX_DIR="$TMP/rootful-override" SQUAREBOX_RUNTIME=docker SQUAREBOX_TAG=v1.1.0 \
PUID=99 PGID=100 "$ROOT/install.sh" </dev/null
grep -qxF 'PUID=99' "$TMP/rootful-override/.squarebox/install-state"
grep -qxF 'PGID=100' "$TMP/rootful-override/.squarebox/install-state"

DARWIN_RUNTIME="$TMP/runtime-darwin-identity"
mkdir -p "$DARWIN_RUNTIME" "$TMP/darwin-home"
HOME="$TMP/darwin-home" MOCK_RUNTIME="$DARWIN_RUNTIME" \
MOCK_UNAME_S=Darwin MOCK_ID_UID=501 MOCK_ID_GID=20 \
SQUAREBOX_DIR="$TMP/darwin-identity" SQUAREBOX_RUNTIME=docker SQUAREBOX_TAG=v1.1.0 \
PUID=1000 PGID=1000 "$ROOT/install.sh" </dev/null
grep -qxF 'PUID=1000' "$TMP/darwin-identity/.squarebox/install-state"
grep -qxF 'PGID=1000' "$TMP/darwin-identity/.squarebox/install-state"

# Noninteractive Selection seeding must not follow a repository-controlled
# Workspace .squarebox link or any known Selection-file link.
SEED_WORKSPACE="$TMP/seed-workspace"
Expand Down Expand Up @@ -299,7 +381,10 @@ grep -qxE '[0-9a-f]{40}' "$SQUAREBOX_DIR/.squarebox/managed-config/lazygit-confi
export HOME="$PRIMARY_HOME" MOCK_RUNTIME="$TMP/runtime"

export SQUAREBOX_DIR="$INSTALL" SQUAREBOX_RUNTIME=docker SQUAREBOX_TAG=v1.1.0 SQUAREBOX_AI=codex
"$ROOT/install.sh" </dev/null
EXPECTED_PUID=$(id -u); EXPECTED_PGID=$(id -g)
[ "$EXPECTED_PUID" -gt 0 ] || EXPECTED_PUID=1000
[ "$EXPECTED_PGID" -gt 0 ] || EXPECTED_PGID=1000
PUID="$EXPECTED_PUID" PGID="$EXPECTED_PGID" "$ROOT/install.sh" </dev/null

STATE="$INSTALL/.squarebox/install-state"
test -f "$STATE"
Expand All @@ -309,13 +394,37 @@ grep -qxF 'SOURCE_COMMIT=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' "$STATE"
grep -qxF 'IMAGE_REF=ghcr.io/squarewavesystems/squarebox@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' "$STATE"
grep -qxF 'IMAGE_DIGEST=ghcr.io/squarewavesystems/squarebox@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' "$STATE"
grep -qxE 'INSTALL_ID=[A-Za-z0-9._-]{8,128}' "$STATE"
grep -qxF "PUID=$EXPECTED_PUID" "$STATE"
grep -qxF "PGID=$EXPECTED_PGID" "$STATE"
EXPECTED_KEYS='BUILD CONTAINER_NAME EDGE FORMAT GIT_CONFIG_DIR HOME_VOLUME HOME_VOLUME_ADOPTED IMAGE_ALIAS IMAGE_DIGEST IMAGE_ID IMAGE_REF IMAGE_REPOSITORY INSTALL_DIR INSTALL_ID ORIGIN PGID PUID RELEASE_TAG REQUESTED_TAG RUNTIME SHELL_INIT SHELL_RC SOURCE_COMMIT SOURCE_REF WORKSPACE_DIR'
ACTUAL_KEYS=$(cut -d= -f1 "$STATE" | sort | tr '\n' ' ' | sed 's/ $//')
[ "$ACTUAL_KEYS" = "$EXPECTED_KEYS" ]
[ "$(wc -l <"$STATE" | tr -d ' ')" = 25 ]
test -f "$INSTALL/.squarebox/identity/git/config"
grep -q 'user.name=Lifecycle Test' "$INSTALL/.squarebox/identity/git/config"
test ! -e "$HOME/.config/git"

# A recorded mismatch is the same unsafe ownership split on rebuild. Refuse it
# before rewriting private config or contacting the runtime.
cp "$STATE" "$TMP/state-before-recorded-mismatch"
sed -i "s/^PUID=.*/PUID=$BAD_UID/; s/^PGID=.*/PGID=$BAD_GID/" "$STATE"
cp "$STATE" "$TMP/state-recorded-mismatch"
git_config_before=$(sha256sum "$INSTALL/.squarebox/identity/git/config")
: >"$MOCK_RUNTIME/calls"
set +e
MOCK_UNAME_S=Linux MOCK_ID_UID="$HOST_UID" MOCK_ID_GID="$HOST_GID" \
"$INSTALL/install.sh" </dev/null >"$TMP/recorded-puid-mismatch.out" 2>&1
recorded_mismatch_rc=$?
set -e
test "$recorded_mismatch_rc" -eq 64
grep -q "requires PUID/PGID to match the invoking host identity ($HOST_UID:$HOST_GID)" \
"$TMP/recorded-puid-mismatch.out"
grep -q "requested $BAD_UID:$BAD_GID" "$TMP/recorded-puid-mismatch.out"
test ! -s "$MOCK_RUNTIME/calls"
test "$git_config_before" = "$(sha256sum "$INSTALL/.squarebox/identity/git/config")"
cmp -s "$STATE" "$TMP/state-recorded-mismatch"
mv "$TMP/state-before-recorded-mismatch" "$STATE"

MOCK_EMPTY_GIT_ID=1 "$INSTALL/install.sh" </dev/null
grep -q 'user.name=Lifecycle Test' "$INSTALL/.squarebox/identity/git/config"
grep -q 'user.email=lifecycle@example.test' "$INSTALL/.squarebox/identity/git/config"
Expand Down Expand Up @@ -472,9 +581,11 @@ if "$ROOT/install.sh" --not-a-real-option >/dev/null 2>&1; then
fi

# Rootless Podman maps the invoking host user to the image's dev identity and
# disables SELinux relabeling for this home-mounting development Box. Private
# :Z labels on Workspace/config/SSH paths make those host paths unusable by
# other consumers and do not compose with this mount model.
# disables SELinux relabeling for this home-mounting development Box. Native
# unprivileged identity mismatches are rejected before runtime inspection; the
# runtime-specific check remains the fail-closed control for root invocation.
# Private :Z labels on Workspace/config/SSH paths make those host paths
# unusable by other consumers and do not compose with this mount model.
export MOCK_RUNTIME="$TMP/runtime-podman"; mkdir -p "$MOCK_RUNTIME"
export MOCK_ROOTLESS=1 MOCK_RELEASE_TAG=v1.1.0 MOCK_MANIFEST_MISSING=0
export HOME="$TMP/podman-home"; mkdir -p "$HOME"
Expand All @@ -487,7 +598,8 @@ grep -q -- '--userns=keep-id:uid=1000,gid=1000' "$MOCK_RUNTIME/calls"
export MOCK_RUNTIME="$TMP/runtime-podman-bad-id"; mkdir -p "$MOCK_RUNTIME"
export HOME="$TMP/podman-bad-id-home"; mkdir -p "$HOME"
export SQUAREBOX_DIR="$TMP/podman-bad-id"
if PUID=99 PGID=100 "$ROOT/install.sh" </dev/null >"$TMP/podman-bad-id.out" 2>&1; then
if MOCK_UNAME_S=Linux MOCK_ID_UID=0 MOCK_ID_GID=0 PUID=99 PGID=100 \
"$ROOT/install.sh" </dev/null >"$TMP/podman-bad-id.out" 2>&1; then
echo 'rootless Podman accepted an ineffective PUID/PGID remap' >&2; exit 1
fi
grep -q 'rootless Podman maps the invoking host identity' "$TMP/podman-bad-id.out"
Expand Down
5 changes: 3 additions & 2 deletions uat-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ Qualification issues: [Linux desktop #126](https://github.com/SquareWaveSystems/

- [ ] Fresh Bash installer: launch, interactive setup, exit, resume, rebuild, uninstall
- [ ] Existing v1.1 Managed home upgrade: no repeated prompts; Selections reconcile
- [ ] Host UID/GID other than 1000: Workspace and managed files remain host-owned
- [ ] Non-default Docker PUID/PGID starts with lifecycle-managed read-only files mounted beneath `/home/dev`
- [ ] Genuine host UID/GID other than 1000: default identity, Workspace, and managed files remain host-owned through rebuild and purge
- [ ] Unprivileged Linux PUID/PGID mismatch fails before checkout, config, Install-state, or runtime mutation; a stale recorded identity can be adopted to the current account
- [ ] Root-run rootful Docker/Podman PUID/PGID override starts with lifecycle-managed read-only files mounted beneath `/home/dev`
- [ ] Custom install path, Workspace, and Managed-home volume survive rebuild and purge correctly
- [ ] Purge refuses an unrelated directory/container/image/volume with a colliding name
- [ ] Docker daemon unavailable during uninstall produces a clear nonzero partial-cleanup result
Expand Down