Files
stack/tools/tmux/test-agent-send-socket-live.sh
T
jason.woltje 2f5a8d2cec feat(skills): skill lifecycle + ms-* skill set completion (#40, #41, #42)
- 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
2026-09-03 16:23:57 -05:00

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