#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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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 ]
|
||||
Reference in New Issue
Block a user