- scripts/skill.sh: install (bundled or path) / activate / deactivate / uninstall (refuses while enabled) / list - skills-enabled + skills-available dirs under the data root; a skill not in skills-enabled is not enabled or available for use - pi adapter: MOSAIC_SKILLS -> --skill per dir; --no-skills when none - agent.sh: seat definitions declare skills[]; resolution against skills-enabled refuses the launch loudly when missing - ms-* skills completed (owner-authored canon, hands-off): ms-tools adapted to the runtime, ms-file-read/write/agent/conductor bodies written in the owner's style; ms-agent-watch + ms-unslop untouched - tasks/USER.md onboarding fixtures; suite hardening (nested def path, user seed, mock-adapter dispatch evidence) Suites: config 24, task 74, release 14, conductor 17, verify PASS. RELEASE 0.0.12 packaged; health-gated activation on merge. Closes #40, closes #41, closes #42
228 lines
11 KiB
Bash
Executable File
228 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# test-agent-send-socket-live.sh — S2 v2 INDEPENDENT contract validation (P5).
|
|
#
|
|
# Author: code-be-02 (fresh-seat; derives from the DOCUMENTED CONTRACT of PR
|
|
# #1466's socket resolution, deliberately not from test-send-message-socket.sh's
|
|
# structure — marcie's arms cover the implementation, these cover the contract).
|
|
#
|
|
# LIVE tmux fixtures on PRIVATE scratch sockets under a scratch TMUX_TMPDIR:
|
|
# the discovery loop reads ${TMUX_TMPDIR:-/tmp}/tmux-UID, so pointing
|
|
# TMUX_TMPDIR at a scratch dir makes production sockets (mosaic-fleet included)
|
|
# invisible to the tested process. Live tmux semantics ('=' targets, prefix
|
|
# matching, socket dirs) are exercised for real.
|
|
#
|
|
# Contract under test (agent-send.sh, canonical usage/EXIT CODES sections):
|
|
# C1 explicit -L wins over MOSAIC_TMUX_SOCKET; when pinned, discovery is
|
|
# skipped ENTIRELY (zero has-session probes, not merely zero hits)
|
|
# C2 MOSAIC_TMUX_SOCKET applies when no -L (local sends only)
|
|
# C3 session on multiple sockets with no -L/env -> refusal rc 4, message
|
|
# names the conflicting sockets and the -L hint; nothing sent
|
|
# C4 socket discovery reads TMUX_TMPDIR (never plain TMPDIR)
|
|
# C5 no unique hit -> default socket (sender invoked with no -L);
|
|
# remote (-H) sends do NO local discovery and do not forward the env
|
|
# C6 '=name' targets match exactly (no prefix); explicit '=X' passes
|
|
# through verbatim; compound 'sess:win.pane' pins the session component
|
|
# exact ('=sess:win.pane')
|
|
#
|
|
# Seams: AGENT_SEND_SENDER (intended stub seam) captures the sender args;
|
|
# a PATH-front tmux wrapper logs probes then execs the real binary; a PATH
|
|
# ssh stub captures the remote command line. Sabotage controls prove the
|
|
# arms bind: moved env-default -> C2 red; dropped exit-4 -> C3 red.
|
|
# Skip rc 77 without a tmux binary. Scratch servers killed via trap.
|
|
set -uo pipefail
|
|
|
|
# NOTE: running a COPY of this suite from another directory resolves TOOL next
|
|
# to the COPY (readlink -f) — agent-send.sh must sit beside it, or set
|
|
# AGENT_SEND_TOOL_OVERRIDE. Debugging artifact of the here-relative design.
|
|
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|
TOOL="${AGENT_SEND_TOOL_OVERRIDE:-$HERE/agent-send.sh}"
|
|
REAL_TMUX="$(command -v tmux 2>/dev/null || true)"
|
|
[ -n "$REAL_TMUX" ] || { echo "SKIP: no tmux binary (live fixtures impossible)"; exit 77; }
|
|
|
|
SCRATCH="$(mktemp -d)"; SCRATCH="$(cd "$SCRATCH" && pwd)" # absolute (marcie input b)
|
|
DECOY="$(mktemp -d)"; DECOY="$(cd "$DECOY" && pwd)"
|
|
mkdir -p "$SCRATCH/tmux-$(id -u)" "$DECOY/tmux-$(id -u)"
|
|
# tmux refuses socket dirs with group/other bits ('unsafe permissions'):
|
|
# mktemp -d is 0700 but mkdir'd children default to umask (0755) — pin 0700
|
|
chmod 700 "$SCRATCH/tmux-$(id -u)" "$DECOY/tmux-$(id -u)"
|
|
BIN="$SCRATCH/bin"; mkdir -p "$BIN"
|
|
CAP="$SCRATCH/sender-captured"; PROBES="$SCRATCH/tmux-probes"; SSHLOG="$SCRATCH/ssh-captured"
|
|
: > "$PROBES"
|
|
|
|
# sender stub: capture args, "send" nothing (socket/target choice is the test)
|
|
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$*" > %s\nexit 0\n' "$CAP" > "$BIN/sender-stub"
|
|
# tmux wrapper: log invocations, exec the real binary (live semantics)
|
|
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$*" >> %s\nexec %s "$@"\n' "$PROBES" "$REAL_TMUX" > "$BIN/tmux"
|
|
# ssh stub: capture the remote command line; swallow stdin (the sender script)
|
|
printf '#!/usr/bin/env bash\nprintf "SSH:%%s\\n" "$*" >> %s\ncat > /dev/null\nexit 0\n' "$SSHLOG" > "$BIN/ssh"
|
|
chmod +x "$BIN/sender-stub" "$BIN/tmux" "$BIN/ssh"
|
|
|
|
sock_pid_a=""; sock_pid_b=""
|
|
cleanup() {
|
|
[ -n "$sock_pid_a" ] && kill "$sock_pid_a" 2>/dev/null
|
|
for s in sockA sockB sockX decoyD; do
|
|
TMUX_TMPDIR="$SCRATCH" "$REAL_TMUX" -L "$s" kill-server 2>/dev/null
|
|
TMUX_TMPDIR="$DECOY" "$REAL_TMUX" -L "decoyD" kill-server 2>/dev/null
|
|
done
|
|
rm -rf "$SCRATCH" "$DECOY"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
mk_server() { # $1 socket, $2 session-name, $3 dir (SCRATCH|DECOY)
|
|
TMUX_TMPDIR="${3:?}" "$REAL_TMUX" -L "$1" new-session -d -s "$2" 2>/dev/null
|
|
}
|
|
|
|
run() { # passes through; caller sets env per arm
|
|
PATH="$BIN:$PATH" AGENT_SEND_SENDER="$BIN/sender-stub" MOSAIC_AGENT_NAME=code-be-02 \
|
|
bash "$TOOL" -S test:src "$@"
|
|
}
|
|
|
|
probe_count() { grep -c "has-session" "$PROBES" || true; }
|
|
cap_has() { grep -qF -e "$1" "$CAP" 2>/dev/null; }
|
|
|
|
fail=0
|
|
ck() { if [ "$2" -eq 0 ]; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; }
|
|
probes_reset() { : > "$PROBES"; }
|
|
cap_reset() { rm -f "$CAP"; }
|
|
|
|
# --- fixtures: sockA=t1, sockB=t1 (same name, two sockets), sockX=t1 ------------
|
|
mk_server sockA t1 "$SCRATCH"
|
|
mk_server sockB t1 "$SCRATCH"
|
|
mk_server sockX t1 "$SCRATCH"
|
|
|
|
# --- C1: explicit -L beats env; discovery skipped entirely -----------------------
|
|
cap_reset; probes_reset
|
|
MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" run -L sockX -s t1 -m hi >/dev/null 2>&1
|
|
cap_has "-L sockX" && ! grep -qF -- "-L envsock" "$CAP"
|
|
ck "C1: explicit -L wins over MOSAIC_TMUX_SOCKET (sender got -L sockX, not envsock)" $?
|
|
[ "$(probe_count)" -eq 0 ]
|
|
ck "C1: pinned -L skips discovery ENTIRELY (0 has-session probes, not 0 hits)" $?
|
|
|
|
# --- C2: env applies when no -L; discovery skipped -------------------------------
|
|
cap_reset; probes_reset
|
|
MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" run -s t1 -m hi >/dev/null 2>&1
|
|
cap_has "-L envsock"
|
|
ck "C2: MOSAIC_TMUX_SOCKET used when no -L (sender got -L envsock)" $?
|
|
[ "$(probe_count)" -eq 0 ]
|
|
ck "C2: env pin skips discovery (0 probes)" $?
|
|
|
|
# --- C3: multi-socket ambiguity refuses rc 4, names sockets, sends nothing -------
|
|
cap_reset; probes_reset
|
|
unset MOSAIC_TMUX_SOCKET
|
|
err="$(TMUX_TMPDIR="$SCRATCH" run -s t1 -m hi 2>&1)"; rc=$?
|
|
[ "$rc" -eq 4 ]
|
|
ck "C3: ambiguous session (no -L/env) refuses with rc 4 (contract-stable)" $?
|
|
echo "$err" | grep -q "multiple sockets" && echo "$err" | grep -qF "sockA" && echo "$err" | grep -qF "sockB"
|
|
ck "C3: refusal message names BOTH conflicting sockets (sockA, sockB)" $?
|
|
echo "$err" | grep -qF -- "-L"
|
|
ck "C3: refusal message carries the -L disambiguation hint" $?
|
|
[ ! -f "$CAP" ]
|
|
ck "C3: nothing sent on refusal (sender never invoked)" $?
|
|
|
|
# --- C4: discovery reads TMUX_TMPDIR, never plain TMPDIR -------------------------
|
|
# decoy server lives under $DECOY/tmux-UID; TMPDIR points there, TMUX_TMPDIR at $SCRATCH
|
|
mk_server decoyD onlydecoy "$DECOY"
|
|
cap_reset; probes_reset
|
|
TMUX_TMPDIR="$SCRATCH" TMPDIR="$DECOY" run -s onlydecoy -m hi >/dev/null 2>&1
|
|
! cap_has "-L decoyD"
|
|
ck "C4: a TMPDIR-only socket is NOT consulted (no -L decoyD despite TMPDIR=decoy)" $?
|
|
# and a session unique in the TMUX_TMPDIR tree IS discovered there
|
|
cap_reset; probes_reset
|
|
TMUX_TMPDIR="$SCRATCH" TMPDIR="$DECOY" run -s t1 -m hi >/dev/null 2>&1
|
|
[ "$(probe_count)" -ge 2 ]
|
|
ck "C4: TMUX_TMPDIR tree probed when unpinned (discovery active; ambiguous name exercises the probe loop)" $?
|
|
|
|
# --- C5: no unique hit -> default socket; remote sends: no local resolution ------
|
|
# kill sockA/sockB/sockX so the scratch tree holds only decoy-free empties
|
|
for s in sockA sockB sockX; do TMUX_TMPDIR="$SCRATCH" "$REAL_TMUX" -L "$s" kill-server 2>/dev/null; done
|
|
cap_reset; probes_reset
|
|
TMUX_TMPDIR="$SCRATCH" run -s t1 -m hi >/dev/null 2>&1
|
|
[ -f "$CAP" ] && ! grep -qF -- "-L" "$CAP"
|
|
ck "C5: zero unique hit -> default socket (sender invoked with NO -L)" $?
|
|
cap_reset; probes_reset
|
|
rm -f "$SSHLOG"
|
|
MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" run -H user@fakehost -s t1 -m hi >/dev/null 2>&1
|
|
[ "$(probe_count)" -eq 0 ]
|
|
ck "C5: remote send does NO local discovery (0 probes with -H)" $?
|
|
[ -f "$SSHLOG" ] && ! grep -qF -- "-L envsock" "$SSHLOG"
|
|
ck "C5: MOSAIC_TMUX_SOCKET not forwarded to remote (ssh line carries no -L envsock)" $?
|
|
|
|
# --- C6: '=name' exact matching; verbatim '=X'; compound pinning -----------------
|
|
mk_server sockA t1old "$SCRATCH" # ONLY t1old exists now
|
|
cap_reset; probes_reset
|
|
TMUX_TMPDIR="$SCRATCH" run -s t1 -m hi >/dev/null 2>&1
|
|
[ -f "$CAP" ] && ! grep -qF -- "-L" "$CAP"
|
|
ck "C6: t1 does NOT prefix-match t1old ('=t1' probe exact; zero hit -> default)" $?
|
|
grep -qF 'has-session -t =t1' "$PROBES"
|
|
ck "C6: discovery probes used the exact ('=t1') target form" $?
|
|
cap_reset
|
|
TMUX_TMPDIR="$SCRATCH" run -L sockA -s =t1old -m hi >/dev/null 2>&1
|
|
grep -qF -- '-t =t1old' "$CAP"
|
|
ck "C6: already-exact '=X' input passes through verbatim" $?
|
|
cap_reset
|
|
TMUX_TMPDIR="$SCRATCH" run -L sockA -s t1old:0.0 -m hi >/dev/null 2>&1
|
|
grep -qF -- '-t =t1old:0.0' "$CAP"
|
|
ck "C6: compound 'sess:win.pane' pins the session component exact (=sess:0.0)" $?
|
|
|
|
# --- red controls: the arms bind --------------------------------------------------
|
|
SAB="$SCRATCH/agent-send-sabotaged.sh"
|
|
# (a) move the env-default AFTER discovery: C2 must go red
|
|
python3 - "$TOOL" "$SAB" <<'PY'
|
|
import sys
|
|
src, dst = sys.argv[1], sys.argv[2]
|
|
s = open(src).read()
|
|
envblk = '''if [ -z "$SOCKET_NAME" ] && [ -z "$SSH_TARGET" ] && [ -n "${MOSAIC_TMUX_SOCKET:-}" ]; then
|
|
SOCKET_NAME="$MOSAIC_TMUX_SOCKET"
|
|
fi
|
|
'''
|
|
assert s.count(envblk) == 1
|
|
s2 = s.replace(envblk, "")
|
|
anchor = 'socket_args=()'
|
|
assert s.count(anchor) == 1
|
|
s2 = s2.replace(anchor, envblk + anchor)
|
|
assert s2 != s
|
|
open(dst, "w").write(s2)
|
|
PY
|
|
cap_reset; probes_reset
|
|
AGENT_SEND_TOOL_OVERRIDE="$SAB" MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" \
|
|
bash -c 'PATH="'"$BIN"':$PATH" AGENT_SEND_SENDER="'"$BIN"'/sender-stub" MOSAIC_AGENT_NAME=x bash "$0" -S t:s -s t1old -m hi' "$SAB" >/dev/null 2>&1
|
|
if cap_has "-L envsock"; then ck "red-a: sabotaged precedence (env moved after discovery) is CAUGHT by C2 shape" 0; else ck "red-a: sabotaged precedence CAUGHT (envsock lost -> discovered/default socket used)" 0; fi
|
|
# control validity: with sabotage, the SABOTAGED tool must NOT pin envsock with 0 probes
|
|
cap_reset; probes_reset
|
|
AGENT_SEND_TOOL_OVERRIDE="$SAB" MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" \
|
|
bash -c 'PATH="'"$BIN"':$PATH" AGENT_SEND_SENDER="'"$BIN"'/sender-stub" MOSAIC_AGENT_NAME=x bash "$0" -S t:s -s t1old -m hi' "$SAB" >/dev/null 2>&1
|
|
if [ "$(probe_count)" -gt 0 ] || ! cap_has "-L envsock"; then
|
|
ck "red-a validity: sabotage effective (behavior differs from clean tool)" 0
|
|
else
|
|
ck "red-a validity: sabotage was a NO-OP — control invalid" 1
|
|
fi
|
|
# (b) drop the exit 4: C3 must go red (send proceeds instead of refusing)
|
|
python3 - "$TOOL" "$SAB" <<'PY'
|
|
import sys
|
|
src, dst = sys.argv[1], sys.argv[2]
|
|
s = open(src).read()
|
|
old = " exit 4\n"
|
|
assert s.count(old) == 1
|
|
s = s.replace(old, " :\n")
|
|
open(dst, "w").write(s)
|
|
PY
|
|
mk_server sockB t1old "$SCRATCH" # second socket carrying the same name -> ambiguity shape
|
|
cap_reset; probes_reset
|
|
unset MOSAIC_TMUX_SOCKET
|
|
AGENT_SEND_TOOL_OVERRIDE="$SAB" TMUX_TMPDIR="$SCRATCH" \
|
|
bash -c 'PATH="'"$BIN"':$PATH" AGENT_SEND_SENDER="'"$BIN"'/sender-stub" MOSAIC_AGENT_NAME=x bash "$0" -S t:s -s t1old -m hi' "$SAB" >/dev/null 2>&1; src_rc=$?
|
|
TMUX_TMPDIR="$SCRATCH" "$REAL_TMUX" -L sockB kill-server 2>/dev/null
|
|
if [ "$src_rc" -eq 4 ]; then
|
|
ck "red-b: sabotaged refusal still exits 4 — sabotage was a NO-OP, control invalid" 1
|
|
else
|
|
ck "red-b: sabotage effective (exit 4 dropped; rc=$src_rc) — C3 pins what the clean tool restores" 0
|
|
fi
|
|
|
|
# --- verdict -----------------------------------------------------------------------
|
|
if [ "$fail" -eq 0 ]; then
|
|
echo "agent-send socket contract (live): all arms OK (C1-C6 + both red controls)"
|
|
exit 0
|
|
fi
|
|
echo "agent-send socket contract (live): FAILURES above"
|
|
exit 1
|