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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.23
0.0.24
39 changes: 35 additions & 4 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,52 @@ opencode-permissions-kit config projects add /var/www/vhosts/new-project
opencode-permissions-kit update --binary
opencode-permissions-kit upgrade-opencode # just the opencode binary
opencode-permissions-kit ddev-hosts-add # in a ddev project dir
opencode-permissions-kit handover me .gotmp # mixed-owner tree -> yours again
opencode-permissions-kit uninstall
opencode-permissions-kit help # commands + arguments overview
```

Everything after the subcommand goes to the underlying script unchanged,
so all flags below work with both forms. `config` and `update` elevate via
sudo automatically; `status` needs no sudo; `uninstall` runs as your user
and asks for sudo itself; `ddev-hosts-*` run as your user (they drive
Windows-side elevation through ddev itself).
so all flags below work with both forms. `config`, `update` and `handover`
elevate via sudo automatically; `status` needs no sudo; `uninstall` runs as
your user and asks for sudo itself; `ddev-hosts-*` run as your user (they
drive Windows-side elevation through ddev itself).

The command is a symlink (`/usr/local/bin/opencode-permissions-kit`) into
the kit library — deployed since kit 0.0.14. On older installs, run
[update](../how-to/update.md) once to get it. The direct script calls below
keep working everywhere.

## handover

Switch file ownership between the two kit users — you and the agent:

```bash
# You and the agent both ran builds in the same checkout; ddev's .gotmp
# cache now contains files of both users and every build complains
# ("chmod ... Operation not permitted"). Make the whole tree yours again:
cd /var/www/vhosts/ddev
opencode-permissions-kit handover me .gotmp

# Same idea the other way — give a folder to the agent user:
opencode-permissions-kit handover opencode /var/www/vhosts/some-project/

# Not sure yet? Show what would happen, without sudo:
opencode-permissions-kit handover me .gotmp --dry-run
```

`me` and `opencode` resolve to your default user and the agent user from
the kit's install configuration — no usernames to remember, no root shell.
Plain `chown -R` would need both.

The change is recursive and only flips the **owner** — the group stays the
kit's sharing group and group-write access is re-applied, so both sides
keep their group access to the tree (the same semantics as the ddev
handovers, see [ddev integration](../concepts/ddev-integration.md)).
System roots (`/`, `/usr`, `/var`, ...) and whole home directories are
refused — hand over trees, not systems. The command elevates via sudo
itself; `--dry-run` validates and prints the plan without sudo.

## ddev-hosts-add / ddev-hosts-check

Windows hosts bridge (WSL2): ddev runs as `opencode` and cannot manage
Expand Down
89 changes: 88 additions & 1 deletion files/opencode-permissions-kit-lib/kit
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ Commands:
that project's missing hostnames, a
hostname adds exactly that one
ddev-hosts-check [dir] List this project's ddev hostnames missing
from the Windows hosts file
from the Windows hosts file
handover <me|opencode> <path>... Recursively hand file ownership to you (me)
or the agent user (opencode) — the sharing
group stays, so both sides keep access. For
mixed-owner trees, e.g. after builds by
both users. --dry-run shows the plan
uninstall [--yes] [--dry-run] Remove the kit (runs as your user)
help Show this help

Expand Down Expand Up @@ -154,6 +159,88 @@ case "$cmd" in
;;
esac
;;
handover)
# Generic ownership switch between the two kit users, for
# mixed-owner trees (e.g. a checkout where BOTH the developer and
# the agent ran builds, like ddev's .gotmp cache): recursive chown
# to <user> + the kit's SHARING group, then g+w — the same
# semantics as the ddev handovers (ddev-handover.sh), applied to
# any path. The group never changes, so both sides keep group
# access; only the owner flips.
# handover me <path>... -> DEFAULT_USER:OPENCODE_GROUP
# handover opencode <path>... -> OPENCODE_USER:OPENCODE_GROUP
# --dry-run prints the plan and changes nothing (no sudo needed).
# System roots and whole home directories are refused: hand over
# trees, not systems. Validation runs BEFORE the sudo hop, so
# argument errors and --dry-run work unprivileged.
_ho_err() {
ui_error "$1"
echo " usage: opencode-permissions-kit handover <me|opencode> <path>... [--dry-run]" >&2
exit 1
}
[ "$#" -ge 2 ] || _ho_err "handover: need a target (me or opencode) and at least one path"
_ho_target="$1"
shift
_ho_dry=0
for _ho_a in "$@"; do
[ "$_ho_a" = "--dry-run" ] && _ho_dry=1
done
# Usage errors first: a wrong target must not be masked by a
# missing install.conf (e.g. dev checkouts without an install).
case "$_ho_target" in
me|opencode) ;;
*) _ho_err "handover: unknown target '$_ho_target' (me or opencode)" ;;
esac
[ -f "$INSTALL_CONF" ] || _ho_err "handover: $INSTALL_CONF not found — is the kit installed?"
_ho_getconf() { sed -n "s/^$1=//p" "$INSTALL_CONF" 2>/dev/null | tail -1; }
_ho_group=$(_ho_getconf OPENCODE_GROUP)
[ -n "$_ho_group" ] || _ho_err "handover: install.conf has no OPENCODE_GROUP"
case "$_ho_target" in
me) _ho_user=$(_ho_getconf DEFAULT_USER) ;;
opencode) _ho_user=$(_ho_getconf OPENCODE_USER) ;;
esac
[ -n "$_ho_user" ] || _ho_err "handover: install.conf has no user for '$_ho_target'"
id -u "$_ho_user" >/dev/null 2>&1 || _ho_err "handover: user '$_ho_user' does not exist"
for _ho_a in "$@"; do
[ "$_ho_a" = "--dry-run" ] && continue
_ho_np=$(realpath -m "$_ho_a" 2>/dev/null || printf '%s' "$_ho_a")
case "$_ho_np" in
/|/bin|/boot|/dev|/etc|/home|/lib|/lib32|/lib64|/libx32|/media|/mnt|/opt|/proc|/root|/run|/sbin|/srv|/sys|/tmp|/usr|/var)
_ho_err "handover: refusing system path '$_ho_np' — hand over subpaths, not system roots" ;;
esac
case "$_ho_np" in
/home/*) [ "$(dirname "$_ho_np")" = "/home" ] && _ho_err "handover: refusing whole home directory '$_ho_np' — hand over subpaths" ;;
esac
[ -e "$_ho_np" ] || _ho_err "handover: no such path: $_ho_a"
done
if [ "$_ho_dry" = 1 ]; then
for _ho_a in "$@"; do
[ "$_ho_a" = "--dry-run" ] && continue
echo " would hand over: $_ho_a -> $_ho_user:$_ho_group (recursive, g+w)"
done
exit 0
fi
if [ "$(id -u)" -ne 0 ]; then
# Loop guard: sudo must actually elevate (fake/broken sudo
# would re-enter this branch forever).
[ "${OPK_HANDOVER_ROOT:-}" = "1" ] && _ho_err "handover: sudo did not elevate — giving up"
exec sudo env OPK_INSTALL_CONF="$INSTALL_CONF" OPK_HANDOVER_ROOT=1 \
sh "$LIBDIR/kit" handover "$_ho_target" "$@"
fi
_ho_rc=0
ui_info "handing over to $_ho_user:$_ho_group (recursive, g+w) ..."
for _ho_a in "$@"; do
[ "$_ho_a" = "--dry-run" ] && continue
if chown -R "$_ho_user:$_ho_group" "$_ho_a" 2>/dev/null && chmod -R g+w "$_ho_a" 2>/dev/null; then
echo " handover: $_ho_a -> $_ho_user:$_ho_group"
else
ui_error "handover failed: $_ho_a"
_ho_rc=1
fi
done
[ "$_ho_rc" = 0 ] && ui_success "handover complete"
exit "$_ho_rc"
;;
help|--help|-h)
usage
;;
Expand Down
89 changes: 87 additions & 2 deletions tests/test-kit-cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ LIB="$WORK/lib"
mkdir -p "$LIB"
# install.conf carries the deployed VERSION stamp (a standalone VERSION file
# is NOT deployed to the library — regression guard for the v0.0.0 display).
printf 'DEFAULT_USER=dev\nVERSION=9.9.9\n' > "$WORK/install.conf"
# Users must EXIST for the handover tests (the command validates with id);
# the current user/group double as the fake developer and opencode user.
printf 'DEFAULT_USER=%s\nOPENCODE_USER=%s\nOPENCODE_GROUP=%s\nVERSION=9.9.9\n' \
"$(id -un)" "$(id -un)" "$(id -gn)" > "$WORK/install.conf"
for s in status config update uninstall; do
cat > "$LIB/$s.sh" <<EOF
#!/bin/sh
Expand Down Expand Up @@ -74,7 +77,7 @@ echo "=== CLI dispatcher tests ==="
# help lists every subcommand
out="$(run_kit --help)"
assert "--help exits 0" "0" "$?"
for c in status config update uninstall help; do
for c in status config update uninstall handover help; do
case "$out" in
*"$c"*) echo " ${GREEN}PASS${NC} --help mentions '$c'"; passed=$((passed + 1)) ;;
*) echo " ${RED}FAIL${NC} --help mentions '$c'"; failures=$((failures + 1)) ;;
Expand Down Expand Up @@ -148,6 +151,88 @@ case "$errout" in
*) echo " ${RED}FAIL${NC} missing script names the problem (got: $errout)"; failures=$((failures + 1)) ;;
esac

# --- handover: generic ownership switch ------------------------------------
# Validation and --dry-run must work unprivileged and BEFORE any sudo hop.
mkdir -p "$WORK/ho-tree/sub"
: > "$WORK/ho-tree/sub/file"

# no / too few arguments -> usage error
if run_kit handover >/dev/null 2>&1; then
echo " ${RED}FAIL${NC} handover without args exits non-zero"; failures=$((failures + 1))
else
echo " ${GREEN}PASS${NC} handover without args exits non-zero"; passed=$((passed + 1))
fi
if run_kit handover me >/dev/null 2>&1; then
echo " ${RED}FAIL${NC} handover without a path exits non-zero"; failures=$((failures + 1))
else
echo " ${GREEN}PASS${NC} handover without a path exits non-zero"; passed=$((passed + 1))
fi

# unknown target -> usage error naming the choices (hermetic: explicit
# install.conf — CI runners have no /etc/opencode-permissions-kit)
errout="$(OPK_INSTALL_CONF="$WORK/install.conf" "$BIN/opencode-permissions-kit" handover nobody "$WORK/ho-tree" 2>&1 >/dev/null || true)"
case "$errout" in
*"me or opencode"*) echo " ${GREEN}PASS${NC} handover rejects unknown target"; passed=$((passed + 1)) ;;
*) echo " ${RED}FAIL${NC} handover rejects unknown target (got: $errout)"; failures=$((failures + 1)) ;;
esac

# nonexistent path -> error
if run_kit handover me "$WORK/does-not-exist" >/dev/null 2>&1; then
echo " ${RED}FAIL${NC} handover rejects nonexistent paths"; failures=$((failures + 1))
else
echo " ${GREEN}PASS${NC} handover rejects nonexistent paths"; passed=$((passed + 1))
fi

# system roots and whole home directories are refused
for _bad in / /usr /etc /var "/home/$(id -un)"; do
errout="$(OPK_INSTALL_CONF="$WORK/install.conf" "$BIN/opencode-permissions-kit" handover me "$_bad" 2>&1 >/dev/null || true)"
case "$errout" in
*"refusing"*) echo " ${GREEN}PASS${NC} handover refuses $_bad"; passed=$((passed + 1)) ;;
*) echo " ${RED}FAIL${NC} handover refuses $_bad (got: $errout)"; failures=$((failures + 1)) ;;
esac
done

# --dry-run: plan only, no changes, no sudo, exit 0
rm -f "$WORK/sudo-marker"
out="$(run_kit handover me "$WORK/ho-tree" --dry-run)"
assert "handover --dry-run exits 0" "0" "$?"
case "$out" in
*"would hand over: $WORK/ho-tree -> $(id -un):$(id -gn)"*)
echo " ${GREEN}PASS${NC} handover --dry-run prints the plan (user:group)"; passed=$((passed + 1)) ;;
*) echo " ${RED}FAIL${NC} handover --dry-run prints the plan (got: $out)"; failures=$((failures + 1)) ;;
esac
if [ -f "$WORK/sudo-marker" ]; then
echo " ${RED}FAIL${NC} handover --dry-run needs no sudo"; failures=$((failures + 1))
else
echo " ${GREEN}PASS${NC} handover --dry-run needs no sudo"; passed=$((passed + 1))
fi

# real run (no --dry-run): elevates via sudo. With the FAKE sudo it cannot
# elevate — the loop guard must stop the re-entry instead of recursing.
rm -f "$WORK/sudo-marker"
if [ "$(id -u)" -ne 0 ]; then
errout="$(OPK_INSTALL_CONF="$WORK/install.conf" "$BIN/opencode-permissions-kit" handover me "$WORK/ho-tree" 2>&1 >/dev/null || true)"
if [ -f "$WORK/sudo-marker" ]; then
echo " ${GREEN}PASS${NC} handover elevates via sudo"; passed=$((passed + 1))
else
echo " ${RED}FAIL${NC} handover elevates via sudo"; failures=$((failures + 1))
fi
case "$errout" in
*"did not elevate"*) echo " ${GREEN}PASS${NC} handover loop guard stops non-elevating sudo"; passed=$((passed + 1)) ;;
*) echo " ${RED}FAIL${NC} handover loop guard stops non-elevating sudo (got: $errout)"; failures=$((failures + 1)) ;;
esac
else
# test env is root: the real chown runs; the tree must end up owned by
# the conf user with group write access.
out="$(run_kit handover opencode "$WORK/ho-tree")"
_own=$(stat -c '%U:%G' "$WORK/ho-tree/sub/file")
assert "handover chowns recursively (root env)" "$(id -un):$(id -gn)" "$_own"
case "$out" in
*"handover: $WORK/ho-tree"*) echo " ${GREEN}PASS${NC} handover reports each path (root env)"; passed=$((passed + 1)) ;;
*) echo " ${RED}FAIL${NC} handover reports each path (root env, got: $out)"; failures=$((failures + 1)) ;;
esac
fi

# List drift guard: install.sh's fetch_kit() list and update.sh's KIT_FILES
# must carry the same file set — a missing entry means streamed installs
# fetch an incomplete kit and crash at deploy time (set -e).
Expand Down