#!/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