#660 install: aipass install no longer hard-exits 2 (silently) when it cannot create global symlinks. setup.sh runs under set -euo pipefail; the #660 safe_symlink refactor returns 2 on ln failure, but the call sites read rc on the NEXT line (rc=$?) — so set -e killed the installer at the symlink step BEFORE the ~/.local/bin fallback (built for exactly the no-sudo case) could run. Any sudo-less env (containers, CI, locked-down machines) got a silent exit 2 + no symlinks despite an otherwise-complete install. Fixed all 3 call sites to rc=0; safe_symlink ... || rc=$? (set-e-safe). Found by the #678 owner-capability docker verify. +tests/docker_owner_verify.sh (owner-capability SOP harness) +tests/_install_diag.sh.

This commit is contained in:
AIOSAI
2026-07-10 01:07:35 -07:00
parent 550839003e
commit 10a8f738a0
4 changed files with 159 additions and 6 deletions
+11
View File
@@ -33,6 +33,17 @@ PyPI version — not the changelog header.
### Fixed
- **`aipass install` no longer hard-fails (exit 2, silently) when it can't create
global symlinks (issue #660 follow-up).** `setup.sh` runs under
`set -euo pipefail`; the #660 `safe_symlink` refactor returns `2` on `ln`
failure, but the call sites captured that code on the *next* line (`rc=$?`), so
`set -e` killed the installer at the symlink step — before the `~/.local/bin`
fallback (built for exactly the no-sudo case) could run. Any sudo-less
environment (containers, CI, locked-down machines) got a silent exit 2 with no
symlinks, despite an otherwise-complete install. Fixed all three call sites to
`rc=0; safe_symlink … || rc=$?` (set-e-safe). Proven in docker: a sudo-less
install now falls back to `~/.local/bin` and exits 0. (devpulse)
- **`drone @devpulse watchdog agent` no longer reports failure on a successful
watch (issue #661).** Its "invoke via Monitor tool" reminder was printed
through `cli.error()`, which — after the #661 exit-code work — trips a
+6 -6
View File
@@ -1017,8 +1017,8 @@ elif [ "$IS_MACOS" -eq 1 ]; then
for cmd in drone aipass; do
if [ -f "$VENV_BIN/$cmd" ]; then
safe_symlink "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd"
rc=$?
rc=0
safe_symlink "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd" || rc=$?
if [ "$rc" -eq 0 ]; then
echo " $LOCAL_BIN/$cmd -> $VENV_BIN/$cmd"
elif [ "$rc" -eq 2 ]; then
@@ -1034,8 +1034,8 @@ else
for cmd in drone aipass; do
if [ -f "$VENV_BIN/$cmd" ]; then
safe_symlink "$VENV_BIN/$cmd" "/usr/local/bin/$cmd" "sudo"
rc=$?
rc=0
safe_symlink "$VENV_BIN/$cmd" "/usr/local/bin/$cmd" "sudo" || rc=$?
if [ "$rc" -eq 0 ]; then
echo " /usr/local/bin/$cmd -> $VENV_BIN/$cmd"
LINUX_SYMLINK_DIR="/usr/local/bin"
@@ -1045,8 +1045,8 @@ else
# sudo/ln failed (e.g. no sudo) — fall back to user-local bin
LOCAL_BIN="$HOME/.local/bin"
mkdir -p "$LOCAL_BIN"
safe_symlink "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd"
rc=$?
rc=0
safe_symlink "$VENV_BIN/$cmd" "$LOCAL_BIN/$cmd" || rc=$?
if [ "$rc" -eq 0 ]; then
echo " /usr/local/bin failed (no sudo) — using $LOCAL_BIN/$cmd instead"
LINUX_SYMLINK_DIR="$LOCAL_BIN"
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Throwaway diagnostic: full ./aipass install output + exit code + symlink state.
set -uo pipefail
cd "$HOME" && rm -rf ws && mkdir ws && cd ws
git clone -b dev --depth 1 https://github.com/AIOSAI/AIPass.git 2>&1 | tail -1
cd AIPass
echo "===== FULL INSTALL OUTPUT ====="
./aipass install
rc=$?
echo "===== INSTALL_EXIT=$rc ====="
echo "===== global symlink state ====="
for p in "$HOME/.local/bin/drone" "$HOME/.local/bin/aipass" /usr/local/bin/drone /usr/local/bin/aipass; do
if [ -L "$p" ]; then echo "SYMLINK $p -> $(readlink "$p")"; elif [ -e "$p" ]; then echo "FILE $p"; else echo "absent $p"; fi
done
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
#
# Owner-capability Dev-Docker verify — SOP artifact for the #678 owner-capability
# model. Runs INSIDE the container (aipass-test image): real GitHub clone of dev,
# one-command install, then proves the owner primitive on a REAL fresh install and
# on a brand-NEW project whose first agent becomes the owner (project manager).
#
# Host invocation:
# docker run --rm -v "<AIPASS>/tests/docker_owner_verify.sh":/verify.sh:ro \
# aipass-test:latest bash /verify.sh
#
set -uo pipefail
PASS=0
FAIL=0
ok() { echo " OK $1"; PASS=$((PASS+1)); }
bad() { echo " FAIL $1"; FAIL=$((FAIL+1)); }
echo "==============================================="
echo " AIPass Owner-Capability Dev-Docker Verify (#678)"
echo "==============================================="
# --- Phase 1: real clone of dev + install ---
echo "--- Phase 1: clone dev + ./aipass install ---"
rm -rf "$HOME/workspace" && mkdir -p "$HOME/workspace" && cd "$HOME/workspace"
if git clone -b dev --depth 1 https://github.com/AIOSAI/AIPass.git 2>&1 | tail -1; then
ok "clone dev"
else
bad "clone dev"; echo "Cannot continue."; exit 1
fi
cd AIPass
AH="$HOME/workspace/AIPass"
echo " HEAD: $(git log -1 --oneline)"
if ./aipass install 2>&1 | tail -5; then ok "installer exit 0"; else bad "installer non-zero"; fi
VPY="$AH/.venv/bin/python3"
DRONE="$AH/.venv/bin/drone"
# --- Phase 2: owner-capability CODE ships in the fresh install ---
echo "--- Phase 2: code ships (resolvers + gate) ---"
if "$VPY" -c "from aipass.spawn.apps.handlers.registry import get_owner, is_owner" 2>/dev/null; then
ok "is_owner/get_owner importable"
else
bad "resolvers not importable"
fi
if [ -f "$AH/src/aipass/hooks/apps/handlers/security/registry_gate.py" ]; then
ok "registry_gate.py present"
else
bad "registry_gate.py missing"
fi
if "$VPY" -c "from aipass.hooks.apps.handlers.security.registry_gate import handle" 2>/dev/null; then
ok "registry_gate importable"
else
bad "registry_gate not importable"
fi
# --- Phase 3: NEW PROJECT — first agent becomes the owner (project manager) ---
echo "--- Phase 3: new project, first agent = owner ---"
PROJ="$HOME/proj_acme"
rm -rf "$PROJ"
cd "$AH/src/aipass/spawn" # run drone from a passport-bearing CWD
"$DRONE" @spawn create "$PROJ/manager" --purpose "Acme project manager" 2>&1 | tail -4
REG=$(find "$PROJ" -name "*_REGISTRY.json" 2>/dev/null | head -1)
if [ -n "$REG" ] && [ -f "$REG" ]; then
ok "new project registry created ($REG)"
else
bad "no registry created under $PROJ"; echo "Cannot continue Phase 3.";
fi
if [ -n "$REG" ]; then
OWNERS=$(jq -r '[.branches[] | select(.owner==true) | .name] | join(",")' "$REG" 2>/dev/null)
if [ "$(echo "$OWNERS" | tr ',' '\n' | grep -c .)" = "1" ]; then ok "exactly one owner ($OWNERS)"; else bad "owner count wrong: [$OWNERS]"; fi
if echo "$OWNERS" | grep -qi "manager"; then ok "owner is the first agent (manager)"; else bad "owner is not manager: [$OWNERS]"; fi
# Resolver against the new project
"$VPY" - "$PROJ" <<'PYEOF'
import sys
from aipass.spawn.apps.handlers.registry import get_owner, is_owner
proj = sys.argv[1]
o = get_owner(proj)
name = (o or {}).get("name"); email = (o or {}).get("email")
print(" OK get_owner(new proj) -> %s (%s)" % (name, email)) if o else print(" FAIL get_owner returned None")
print(" OK is_owner(manager)=True") if is_owner(email or "@manager", proj) else print(" FAIL is_owner(owner) False")
print(" FAIL is_owner(@nobody)=True (should be False)") if is_owner("@nobody_xyz", proj) else print(" OK is_owner(@nobody)=False")
PYEOF
fi
# --- Phase 4: second agent is NOT the owner ---
echo "--- Phase 4: second agent stays non-owner ---"
"$DRONE" @spawn create "$PROJ/worker" --purpose "Acme worker" 2>&1 | tail -2
if [ -n "$REG" ]; then
OWNERS2=$(jq -r '[.branches[] | select(.owner==true) | .name] | join(",")' "$REG" 2>/dev/null)
CNT2=$(jq -r '.branches | length' "$REG" 2>/dev/null)
if [ "$CNT2" = "2" ]; then ok "2 agents in project"; else bad "agent count: $CNT2 (want 2)"; fi
if echo "$OWNERS2" | grep -qi "manager" && [ "$(echo "$OWNERS2" | tr ',' '\n' | grep -c .)" = "1" ]; then ok "owner still only manager after 2nd agent"; else bad "owner drifted: [$OWNERS2]"; fi
"$VPY" - "$PROJ" <<'PYEOF'
import sys
from aipass.spawn.apps.handlers.registry import is_owner
proj = sys.argv[1]
print(" FAIL worker is_owner=True (should be False)") if is_owner("@worker", proj) else print(" OK is_owner(@worker)=False")
PYEOF
fi
# --- Phase 5: gate seals the new project's registry ---
echo "--- Phase 5: registry_gate blocks raw write, allows drone @spawn ---"
"$VPY" - "$REG" <<'PYEOF'
import sys
from aipass.hooks.apps.handlers.security.registry_gate import handle
reg = sys.argv[1] if len(sys.argv) > 1 else "AIPASS_REGISTRY.json"
def R(tn, ti):
r = handle({"tool_name": tn, "tool_input": ti})
return "BLOCK" if r.get("exit_code") == 2 else "ALLOW"
cases = [
("raw write blocked", R("Bash", {"command": "echo x > %s" % reg}) == "BLOCK"),
("Edit tool blocked", R("Edit", {"file_path": reg}) == "BLOCK"),
("drone @spawn allowed",R("Bash", {"command": "drone @spawn create %s" % reg}) == "ALLOW"),
("read (cat) allowed", R("Bash", {"command": "cat %s" % reg}) == "ALLOW"),
]
for name, good in cases:
print(" OK " + name) if good else print(" FAIL " + name)
PYEOF
# --- Summary ---
echo "==============================================="
echo " Results: $PASS passed, $FAIL failed (bash) + inline python OK/FAIL above"
echo "==============================================="
[ "$FAIL" -eq 0 ]