fix(tmux): explicit transport-only dispatch and safe remote quoting (#1496)

This commit is contained in:
2026-09-08 12:41:25 -05:00
parent 67eaf6fb47
commit 69f10a4062
71 changed files with 6564 additions and 316 deletions
+22 -4
View File
@@ -28,7 +28,7 @@ Example exchange:
## The helper: `agent-send.sh`
Prepends the preamble automatically (auto-detecting your own `host:session`) and
delivers reliably to local OR remote panes.
dispatches transport to local OR remote panes; application acceptance remains unknown.
```bash
# Local target (same host, default tmux server)
@@ -47,7 +47,7 @@ echo "msg" | agent-send.sh -s <dst_session>
Key flags: `-L` named tmux socket · `-s` dst session (required) · `-H` ssh target for remote · `-n` dst
hostname for the preamble (else auto-resolved) · `-m`/`-f`/stdin body · `-S`
override source label · `-v` verbose · `-r N` Enter-flush attempts.
override source label · `-v` transport metadata only · `-r N` compatibility-only, no retries.
For durable fleet use, prefer exact tmux targets such as `=coder0`. The helper
normalizes exact session targets to pane-qualified targets internally so pane
@@ -75,13 +75,31 @@ plain `Enter` keyname often does not register at all — `C-m` is needed.
`send-message.sh` solves this for a **local** pane: bracketed-paste the body
(so multi-line content doesn't submit early), pause, then send `Enter` as its own
keystroke and flush with a second, verifying against a draft heuristic.
keystroke. It does not send automatic extra Enters. The legacy `-r` option
is accepted for compatibility but no longer authorizes flushes.
Jason approved the **transport-only contract** after independent review demonstrated
that screen layouts cannot prove application acceptance. Exit 0 means tmux accepted
buffer load, one paste and one Enter command, not that the application submitted,
queued or processed the message. Output explicitly says:
`transport dispatched; application acceptance unknown`.
No capture-pane or editor/footer parser participates in transport success. Hidden
retained drafts can coexist with successful transport; they are never called
confirmed delivery. Failure exits remain nonzero (1 target resolution, 2 transport
failed/partial/uncertain, 3 usage; wrapper 4 ambiguous socket). Failed paste is not
retried with another mode. Never blindly replay a partial/uncertain operation.
Verbose output is content-free transport metadata. Runtime-bound receipts are
separate future work, not implemented by this tool.
`agent-send.sh` solves the **remote** case by _shipping `send-message.sh` over ssh_
(`ssh host bash -s -- ... < send-message.sh`) and running it local to the target
pane — so the reliable send-keys always happens on the pane's own host. The remote
needs only `bash` + `tmux` + `base64`; **no mosaic install required there**. The
message crosses the wire as base64 (`-b`) to avoid all shell-quoting hazards.
message crosses the wire as base64 (`-b`). Every remote command argument is
independently POSIX-shell quoted; retry syntax is validated before invoking SSH.
The executing-shell regression suite checks quote-bearing target/socket arguments
and rejects an invalid retry before any SSH call.
## Files
+20 -7
View File
@@ -20,7 +20,7 @@
# Reliable submission into an interactive REPL (Claude Code / Codex) is fiddly:
# a trailing Enter is often swallowed and the message sits as an unsubmitted
# DRAFT. tools/tmux/send-message.sh already solves that for a LOCAL pane via
# bracketed-paste + Enter-flush + draft-detection. For REMOTE targets this
# checked bracketed paste + one Enter (transport only). For REMOTE targets this
# wrapper SHIPS send-message.sh over ssh (stdin) and runs it there, so the
# reliable send-keys happens local to the target pane — sidestepping the
# ssh->nested-tmux Enter/C-m swallow entirely. No mosaic install needed on
@@ -57,8 +57,8 @@
# byte-for-byte identical to the classic format. Consumers MUST
# treat an absent class as 'actionable' (fail-safe: agent sees it).
# -S SRC_LABEL override source label "<host>:<session>" (default: auto)
# -r N Enter-flush attempts passed through (default 2)
# -v verbose: print pane tail after delivery
# -r N Legacy compatibility option; no automatic extra Enter
# -v verbose: transport metadata only, no private pane contents
# -h help
#
# PREAMBLE GRAMMAR (for consumers / daemons mirroring this producer)
@@ -67,7 +67,8 @@
# group 3 = class (absent => actionable) group 4 = message body
#
# EXIT CODES (passed through from send-message.sh, except 4)
# 0 delivered/queued · 1 target not found · 2 still draft · 3 usage error
# 0 transport dispatched; application acceptance unknown · 1 target not found
# 2 transport failed/partial/uncertain · 3 usage error
# 4 agent-send refusal: local target session exists on multiple socket
# servers and no -L / MOSAIC_TMUX_SOCKET disambiguated it (B1)
set -uo pipefail
@@ -106,6 +107,7 @@ while getopts "L:s:H:n:m:f:S:r:C:vh" o; do
esac
done
[[ "$RETRIES" =~ ^[0-9]+$ ]] || { echo 'ERROR: -r requires a nonnegative integer' >&2; exit 3; }
[ -n "$DST_SESSION" ] || { echo "ERROR: -s DST_SESSION is required" >&2; usage 3; }
[ -x "$SENDER" ] || { echo "ERROR: send-message.sh not found beside this script" >&2; exit 3; }
@@ -201,7 +203,10 @@ if [ -z "$SOCKET_NAME" ] && [ -z "$SSH_TARGET" ]; then
# '=' forces exact session-name matching: tmux target syntax otherwise
# accepts an unambiguous PREFIX, so a session named X-old on a socket
# would count as a false hit for target X (codex PR #1466).
tmux -L "$sname" has-session -t "$DST_TARGET" 2>/dev/null && hits="$hits$sname"$'\n'
# Silence BOTH streams: has-session writes nothing to stdout, but a stub
# (test fake) may — leaked probe stdout polluted this tool's stdout and
# broke callers that read it (measured 2026-09-07, agent-send.test #9b).
tmux -L "$sname" has-session -t "$DST_TARGET" >/dev/null 2>&1 && hits="$hits$sname"$'\n'
done
hit_count=$(printf '%s' "$hits" | grep -c . || true)
if [ "$hit_count" -gt 1 ]; then
@@ -224,6 +229,14 @@ if [ -z "$SSH_TARGET" ]; then
exec "$SENDER" "${socket_args[@]}" -t "$DST_TARGET" -b "$B64" -r "$RETRIES" $vflag
else
# Remote pane: ship the sender over ssh and run it local to the target.
ssh -o ConnectTimeout=10 "$SSH_TARGET" \
"bash -s -- ${socket_args[*]@Q} -t '$DST_TARGET' -b '$B64' -r '$RETRIES' $vflag" < "$SENDER"
# SSH passes a command string through the remote login shell. Quote EACH
# argument with POSIX single-quote escaping before that shell parses it.
remote_args=(bash -s -- "${socket_args[@]}" -t "$DST_TARGET" -b "$B64" -r "$RETRIES")
[ "$VERBOSE" = 0 ] || remote_args+=(-v)
remote_command=""
for arg in "${remote_args[@]}"; do
escaped=${arg//\'/\'\\\'\'}
remote_command+=" '$escaped'"
done
ssh -o ConnectTimeout=10 "$SSH_TARGET" "$remote_command" < "$SENDER"
fi
+9 -3
View File
@@ -28,7 +28,7 @@ TOOL="$HERE/agent-send.sh"
# Capture stub: stands in for send-message.sh. Decodes -b and prints the payload.
STUB=$(mktemp)
FAKE_BIN=$(mktemp -d)
trap 'rm -f "$STUB"; rm -rf "$FAKE_BIN"' EXIT
trap 'rm -f "$STUB"; rm -rf "$FAKE_BIN" "$SCRATCH_TMPDIR"' EXIT
cat >"$STUB" <<'STUB_EOF'
#!/usr/bin/env bash
set -uo pipefail
@@ -62,9 +62,15 @@ no() { FAIL=$((FAIL+1)); printf 'FAIL %s\n %s\n' "$1" "$2"; }
# Run the tool with the stub injected; echoes captured payload on stdout.
run() { AGENT_SEND_SENDER="$STUB" bash "$TOOL" -S a:src -n dsthost "$@"; }
# Hermetic auto-label runs: TMUX is controlled explicitly so results never
# depend on whether the caller running this suite sits inside tmux.
# depend on whether the caller running this suite sits inside tmux — and
# TMUX_TMPDIR is pinned to an empty scratch dir so socket discovery never
# sees the HOST's sockets (measured 2026-09-07: with default+mosaic-fleet
# live, discovery saw the fake answer 'mos' on both and B1-refused rc 4
# before the stub ever ran; a one-socket host passed, so this only bites
# multi-socket hosts).
SCRATCH_TMPDIR=$(mktemp -d)
run_auto() { # models a sender OUTSIDE tmux (no client context)
env -u MOSAIC_AGENT_NAME -u TMUX \
env -u MOSAIC_AGENT_NAME -u TMUX TMUX_TMPDIR="$SCRATCH_TMPDIR" \
AGENT_SEND_SENDER="$STUB" PATH="$FAKE_BIN:$PATH" \
bash "$TOOL" -n dsthost "$@"
}
+49 -171
View File
@@ -1,181 +1,59 @@
#!/usr/bin/env bash
# send-message.sh — reliably deliver a message to a tmux pane running an
# interactive REPL (e.g. a Claude Code / Codex agent).
# send-message.sh — dispatch text through tmux; application acceptance unknown.
#
# WHY THIS EXISTS
# Pasting multi-line text into an interactive agent REPL via `tmux send-keys`
# is unreliable: the text lands in the input box but a single trailing Enter
# in the same keystroke stream is frequently swallowed, so the message sits as
# an UNSUBMITTED DRAFT ("Press up to edit queued messages") and the agent never
# sees it. The mechanical fix is: paste as a bracketed paste (so embedded
# newlines don't submit early), pause, then send Enter as its OWN keystroke,
# pause, and send Enter again to flush. An extra Enter on an empty prompt is a
# no-op in Claude Code, so the double-Enter is safe.
#
# USAGE
# send-message.sh [-L socket_name] -t <target> -m "message"
# send-message.sh [-L socket_name] -t <target> -f <file>
# echo "message" | send-message.sh [-L socket_name] -t <target>
# ssh host bash -s -- -L socket -t <target> -b "$(base64 -w0 <<<msg)" < send-message.sh
#
# OPTIONS
# -L NAME tmux socket name passed to `tmux -L NAME` (optional)
# -t TARGET tmux target: session, or session:window.pane [required]
# -m MESSAGE message text (single- or multi-line)
# -f FILE read message from FILE instead of -m
# -b BASE64 message as base64 (ssh-safe transport; decoded internally)
# -r N Enter-flush attempts (default 2)
# -v verbose: print a short tail of the pane after delivery
# -h help
#
# EXIT CODES
# 0 delivered (submitted) or queued (agent busy; will process when free)
# 1 tmux target not found
# 2 submission NOT confirmed — either still an unsubmitted draft, or the REPL
# input box could not be located to confirm the message actually landed.
# Locating the box is runtime-specific; see locate_input_box() below, and
# add a shape there before pointing this tool at a new runtime.
# Delivery is NEVER inferred from absence of evidence: if we cannot positively
# see the input box clear of the message (or the queued banner), we fail loud
# so the sender learns immediately instead of a silent worker->lead stall.
# 3 usage error
# Usage: send-message.sh [-L socket] -t target {-m message|-f file|-b base64}
# With no message option, reads stdin. Requires bash, tmux and base64.
# -r N is compatibility-only: no automatic retries or extra Enter presses.
# -v prints transport metadata only, never a captured private transcript.
# Exit 0: tmux accepted buffer load, paste and one Enter command.
# Exit 1: target resolution failed. Exit 2: transport failed/partial/uncertain.
# Exit 3: invalid usage/input. No exit establishes application acknowledgement.
set -uo pipefail
SOCKET_NAME=""; TARGET=""; MSG=""; FILE=""; B64=""; RETRIES=2; VERBOSE=0
usage() { sed -n '2,34p' "$0"; exit "${1:-3}"; }
while getopts "L:t:m:f:b:r:vh" o; do
SOCKET_NAME=""; TARGET=""; MSG=""; FILE=""; B64=""; VERBOSE=0
usage() { printf '%s\n' 'Usage: send-message.sh [-L socket] -t target [-m message|-f file|-b base64] [-r N] [-v]' 'Exit 0 = transport dispatched; application acceptance unknown. No automatic retries.'; exit "${1:-3}"; }
while getopts 'L:t:m:f:b:r:vh' o; do
case "$o" in
L) SOCKET_NAME=$OPTARG ;;
t) TARGET=$OPTARG ;; m) MSG=$OPTARG ;; f) FILE=$OPTARG ;; b) B64=$OPTARG ;;
r) RETRIES=$OPTARG ;; v) VERBOSE=1 ;; h) usage 0 ;; *) usage 3 ;;
L) SOCKET_NAME=$OPTARG ;; t) TARGET=$OPTARG ;; m) MSG=$OPTARG ;;
f) FILE=$OPTARG ;; b) B64=$OPTARG ;;
r) [[ "$OPTARG" =~ ^[0-9]+$ ]] || usage 3 ;;
v) VERBOSE=1 ;; h) usage 0 ;; *) usage 3 ;;
esac
done
[ -n "$TARGET" ] || { echo "ERROR: -t TARGET is required" >&2; usage 3; }
if [ -n "$B64" ]; then MSG=$(printf '%s' "$B64" | base64 -d) || { echo "ERROR: bad -b base64" >&2; exit 3; }
elif [ -n "$FILE" ]; then [ -r "$FILE" ] || { echo "ERROR: cannot read $FILE" >&2; exit 3; }; MSG=$(cat -- "$FILE")
elif [ -z "$MSG" ] && [ ! -t 0 ]; then MSG=$(cat)
shift "$((OPTIND - 1))"
[ "$#" -eq 0 ] && [ -n "$TARGET" ] || usage 3
if [ -n "$B64" ]; then
MSG=$(printf '%s' "$B64" | base64 -d) || { echo 'ERROR: invalid base64' >&2; exit 3; }
elif [ -n "$FILE" ]; then
MSG=$(cat -- "$FILE") || { echo 'ERROR: cannot read message file' >&2; exit 3; }
elif [ -z "$MSG" ] && [ ! -t 0 ]; then
MSG=$(cat) || exit 3
fi
[ -n "$MSG" ] || { echo "ERROR: empty message (use -m, -f, or stdin)" >&2; exit 3; }
[ -n "$MSG" ] || { echo 'ERROR: empty message' >&2; exit 3; }
tmux_cmd=(tmux)
if [ -n "$SOCKET_NAME" ]; then
tmux_cmd+=(-L "$SOCKET_NAME")
fi
# tmux accepts `=session` for some commands, but pane-level commands such as
# capture-pane require a pane-qualified target. Keep exact-session addressing
# convenient while avoiding accidental prefix matches.
[ -z "$SOCKET_NAME" ] || tmux_cmd+=(-L "$SOCKET_NAME")
EFFECTIVE_TARGET=$TARGET
if [[ "$TARGET" == =* && "$TARGET" != *:* ]]; then
EFFECTIVE_TARGET="${TARGET}:0.0"
fi
# Target must resolve to a live pane.
if ! "${tmux_cmd[@]}" list-panes -t "$EFFECTIVE_TARGET" >/dev/null 2>&1; then
echo "ERROR: tmux target not found: $TARGET" >&2; exit 1
fi
QUEUED_RE='Press up to edit queued messages'
# A distinctive tail of the message to spot an unsubmitted draft on the input line.
snippet=$(printf '%s' "$MSG" | tr '\n' ' ' | tr -s ' ' | sed 's/[^[:print:]]//g' | tail -c 32)
# 1) Paste the body as a bracketed paste so multi-line content does not submit
# line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`.
# Buffer name MUST be unique per invocation: concurrent senders on the shared
# tmux server race a fixed name (load overwrites load, -d deletes underneath),
# cross-delivering or dropping messages — bit the fleet on the 2026-07-09
# simultaneous restart (briefs swapped between sessions).
BUF="__mosaic_send_$$_$(date +%s%N)"
printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -
# -p = bracketed paste when the client supports it; fall back if not.
"${tmux_cmd[@]}" paste-buffer -d -p -b "$BUF" -t "$EFFECTIVE_TARGET" 2>/dev/null \
|| "${tmux_cmd[@]}" paste-buffer -d -b "$BUF" -t "$EFFECTIVE_TARGET" \
|| "${tmux_cmd[@]}" delete-buffer -b "$BUF" 2>/dev/null
# ^ -d deletes the buffer only on a SUCCESSFUL paste; if both attempts fail
# (e.g. the target vanished since the liveness check), delete explicitly —
# named buffers are exempt from tmux's buffer-limit eviction, so orphans
# would otherwise accumulate forever.
sleep 0.5
# Locate the REPL input box in a captured pane. Prints the box's contents on
# stdout and returns 0 when the box was FOUND; returns 1 when it could not be
# located at all. Found-but-empty is a real, distinct answer (an empty input box
# is what a submitted message leaves behind), so the caller must branch on the
# return code, never on whether the output is empty.
#
# Two REPL shapes are recognised:
# * a prompt-glyph line — ``, a leading `>`, or `│ >`. Claude Code and most
# readline REPLs.
# * a box drawn as two horizontal `─` rules with the input between them and NO
# prompt glyph anywhere. pi renders this. Anchoring on the LAST rule pair is
# what makes it safe: agent output can contain its own rules, but nothing is
# drawn below the input box except the status line.
#
# Adding a runtime means adding its shape HERE. A shape that is missing does not
# degrade gracefully: it turns every send to that runtime into a false
# "may be UNDELIVERED", which is what #1362 measured on pi and #1257 on another
# arm of the same probe.
locate_input_box() {
local pane=$1 glyph_line rule_lines top bottom
glyph_line=$(printf '%s\n' "$pane" | grep -E '|^>|│ >' | tail -1)
if [ -n "$glyph_line" ]; then printf '%s\n' "$glyph_line"; return 0; fi
rule_lines=$(printf '%s\n' "$pane" | grep -nE '^[[:space:]]*─{4,}[[:space:]]*$' | cut -d: -f1 | tail -2)
[ -n "$rule_lines" ] || return 1
# Split the (at most two) captured line numbers with parameter expansion. Not
# `head -1`: piping into an early-exiting consumer SIGPIPEs the producer, which
# under `set -euo pipefail` aborts the caller with rc=141 and no output. The
# scripts/pipefail-early-exit.test.mjs guard reds on that shape, correctly.
# With one rule captured both halves resolve to the same value and the
# ordering test below rejects it, which is the answer we want anyway.
top=${rule_lines%%$'\n'*}
bottom=${rule_lines##*$'\n'}
[ "$top" != "$bottom" ] || return 1
[ "$bottom" -gt "$top" ] || return 1
# An empty range (adjacent rules) prints nothing and still returns 0: found,
# empty, which is the delivered shape.
printf '%s\n' "$pane" | sed -n "$((top + 1)),$((bottom - 1))p"
return 0
if [[ "$TARGET" == =* && "$TARGET" != *:* ]]; then EFFECTIVE_TARGET="${TARGET}:0.0"; fi
# Pin one pane ID for all subsequent commands rather than resolving a moving
# session/window target independently at every transport step.
PANE=$("${tmux_cmd[@]}" display-message -p -t "$EFFECTIVE_TARGET" '#{pane_id}' 2>/dev/null) || {
echo 'ERROR: tmux target resolution failed' >&2; exit 1;
}
# 2) Submit, then POSITIVELY confirm submission; flush with another Enter if it is
# still a draft. Success requires positive evidence — the queued banner, OR the
# REPL input box located AND clear of our message tail. The historical bug was
# treating ABSENCE of a draft as delivery: if the input box was never located
# (wrong pane / prompt-glyph drift), an unsubmitted message read as "delivered"
# and worker->lead relays stalled silently. We now default to UNCONFIRMED and only
# upgrade to delivered on positive evidence; anything we cannot confirm fails loud.
status="unconfirmed"
for attempt in $(seq 1 $((RETRIES + 1))); do
"${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter
sleep 1.2
pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null)
if grep -qF "$QUEUED_RE" <<<"$pane"; then
status="queued"; break
fi
# If we cannot see the input box, we have NO evidence of submission state —
# stay UNCONFIRMED and retry; never infer delivery.
if ! inputbox=$(locate_input_box "$pane"); then
status="unconfirmed"; continue
fi
# Input box located AND still carrying our tail => unsubmitted draft. Flush + retry.
# (Submitted messages scroll up into history; a draft stays in the box.)
if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$inputbox"; then
status="draft"; continue
fi
# Input box located AND clear of our tail => positively submitted. This is the
# only path to success besides the queued banner.
status="delivered"; break
done
[ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; }
case "$status" in
delivered) echo "✓ delivered to $TARGET"; exit 0 ;;
queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;;
draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;;
unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input box not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;;
*) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;;
esac
[[ "$PANE" =~ ^%[0-9]+$ ]] || { echo 'ERROR: invalid resolved pane identity' >&2; exit 1; }
BUF="__mosaic_send_$$_$(date +%s%N)"
cleanup() { "${tmux_cmd[@]}" delete-buffer -b "$BUF" >/dev/null 2>&1 || true; }
trap cleanup EXIT
if ! printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -; then
echo 'ERROR: buffer load failed; transport incomplete' >&2; exit 2
fi
# Do not retry a failed paste: failure may be partial. Bracketed paste is
# requested once; changing paste mode after failure could duplicate effects.
if ! "${tmux_cmd[@]}" paste-buffer -d -p -b "$BUF" -t "$PANE"; then
echo 'ERROR: paste failed; transport uncertain; do not blindly resend' >&2; exit 2
fi
sleep 0.5
if ! "${tmux_cmd[@]}" send-keys -t "$PANE" Enter; then
echo 'ERROR: submission key failed; transport partial; do not blindly resend' >&2; exit 2
fi
[ "$VERBOSE" -eq 0 ] || printf 'transport pane=%s; paste_calls=1; submission_keys=1\n' "$PANE"
printf '%s\n' 'transport dispatched; application acceptance unknown'
exit 0
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Execute the SSH command through a shell; no network or real tmux access."""
import json
import os
from pathlib import Path
import subprocess
import tempfile
here = Path(__file__).resolve().parent
with tempfile.TemporaryDirectory(prefix="tmux-remote-contract-") as temp:
root = Path(temp)
bindir = root / "bin"
bindir.mkdir()
(bindir / "ssh").write_text("#!/usr/bin/env python3\nimport os,subprocess,sys\nopen(os.environ['SSH_CALLS'],'a').write('ssh\\n')\nraise SystemExit(subprocess.call(['/bin/sh','-c',sys.argv[-1]]))\n")
(bindir / "tmux").write_text("#!/usr/bin/env python3\nimport json,os,sys\na=sys.argv[1:]\nwith open(os.environ['CALLS'],'a') as f: f.write(json.dumps(a)+'\\n')\nif a and a[0]=='-L': a=a[2:]\nif a[0]=='display-message': print('%7')\nif a[0]=='load-buffer': open(os.environ['PAYLOAD'],'wb').write(sys.stdin.buffer.read())\n")
for path in bindir.iterdir():
path.chmod(0o755)
env = dict(os.environ, PATH=str(bindir) + ':' + os.environ['PATH'], CALLS=str(root/'calls'), SSH_CALLS=str(root/'ssh-calls'), PAYLOAD=str(root/'payload'))
env.pop('AGENT_SEND_SENDER', None)
attack = "x'; printf 'UNAUTHORIZED_EXTRA_EFFECT\\n'; #"
cases = [('target', ['-s', attack], 0), ('socket', ['-s', 'fixture', '-L', attack], 0), ('retry', ['-s', 'fixture', '-r', attack], 3)]
for label, args, expected in cases:
(root/'calls').write_text('')
(root/'ssh-calls').write_text('')
result = subprocess.run(['bash', str(here/'agent-send.sh'), '-S', 'review:src', '-n', 'fake', '-H', 'fake', '-m', 'safe\n你好'] + args, env=env, text=True, capture_output=True)
assert result.returncode == expected, (label, result)
assert 'UNAUTHORIZED_EXTRA_EFFECT' not in result.stdout, (label, result.stdout)
calls = [json.loads(line) for line in (root/'calls').read_text().splitlines()]
if expected == 3:
assert not calls and not (root/'ssh-calls').read_text()
else:
assert result.stdout.strip() == 'transport dispatched; application acceptance unknown'
command_calls = [a[2:] if a[0] == '-L' else a for a in calls]
assert sum(a[0] == 'paste-buffer' for a in command_calls) == 1
assert sum(a[0] == 'send-keys' for a in command_calls) == 1
assert b'safe\n' in (root/'payload').read_bytes()
if label == 'socket': assert all(a[:2] == ['-L', attack] for a in calls)
if label == 'target': assert command_calls[0][3] == '=' + attack + ':0.0'
print('PASS remote', label)
+4 -2
View File
@@ -73,7 +73,8 @@ require_tmux
tmux -L "$SOCKET" new-session -d -s "$TARGET" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
tmux new-session -d -s "$DEFAULT_TARGET" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
"$SEND_MESSAGE" -L "$SOCKET" -t "=$TARGET" -m "named socket hello" >/tmp/send-message-named.out
"$SEND_MESSAGE" -L "$SOCKET" -t "=$TARGET" -m "named socket hello" >"$TMPDIR/send-message-named.out"
grep -qx 'transport dispatched; application acceptance unknown' "$TMPDIR/send-message-named.out" || fail 'missing transport-only result'
sleep 0.2
named_pane="$(capture_named)" || fail "could not capture named socket pane"
grep -qF "named socket hello" <<<"$named_pane" || fail "send-message.sh did not deliver to named socket"
@@ -82,7 +83,8 @@ if grep -qF "named socket hello" <<<"$default_pane"; then
fail "send-message.sh leaked named-socket message to default tmux server"
fi
"$AGENT_SEND" -L "$SOCKET" -S "tester:source" -s "=$TARGET" -m "agent socket hello" >/tmp/agent-send-named.out
"$AGENT_SEND" -L "$SOCKET" -S "tester:source" -s "=$TARGET" -m "agent socket hello" >"$TMPDIR/agent-send-named.out"
grep -qx 'transport dispatched; application acceptance unknown' "$TMPDIR/agent-send-named.out" || fail 'wrapper changed transport-only result'
sleep 0.2
named_pane="$(capture_named)" || fail "could not capture named socket pane"
grep -qF "[tester:source ->" <<<"$named_pane" || fail "agent-send.sh did not include preamble"
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Deterministic transport-only contract tests; no live pane or model access.
set -euo pipefail
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
mkdir "$TMP/bin"
cat > "$TMP/bin/tmux" <<'FAKE'
#!/usr/bin/env bash
[ "${1:-}" != -L ] || shift 2
printf '%s\n' "$1" >> "$CALLS"
case "$1" in
display-message) [ "$FAIL_OP" != target ] || exit 1; printf '%%7\n' ;;
load-buffer) cat > "$PAYLOAD"; [ "$FAIL_OP" != load ] || exit 1 ;;
paste-buffer) [ "$FAIL_OP" != paste ] || exit 1 ;;
send-keys) [ "$FAIL_OP" != key ] || exit 1 ;;
capture-pane) echo 'ERROR: transport must not inspect application display' >&2; exit 99 ;;
esac
FAKE
chmod +x "$TMP/bin/tmux"
body=$'你好 transport\nsecond line'
for mode in none target load paste key; do
: > "$TMP/calls"
set +e
PATH="$TMP/bin:$PATH" CALLS="$TMP/calls" PAYLOAD="$TMP/payload" FAIL_OP="$mode" \
bash "$HERE/send-message.sh" -L isolated -t '=fixture' -r 999 -v -m "$body" > "$TMP/out" 2> "$TMP/err"
rc=$?
set -e
expected=2; [ "$mode" != none ] || expected=0; [ "$mode" != target ] || expected=1
[ "$rc" -eq "$expected" ]
! grep -q capture-pane "$TMP/calls"
keys=$(grep -c '^send-keys$' "$TMP/calls" || true)
pastes=$(grep -c '^paste-buffer$' "$TMP/calls" || true)
[ "$keys" -le 1 ] && [ "$pastes" -le 1 ]
if [ "$mode" = none ]; then
grep -qx 'transport dispatched; application acceptance unknown' "$TMP/out"
[ "$keys" -eq 1 ] && [ "$pastes" -eq 1 ]
[ "$(<"$TMP/payload")" = "$body" ]
else
! grep -q 'transport dispatched' "$TMP/out"
fi
echo "PASS transport $mode exit=$rc paste=$pastes keys=$keys"
done
+6 -129
View File
@@ -1,131 +1,8 @@
#!/usr/bin/env bash
# test-send-message-verdict.sh — locks the fail-loud verdict logic of the patched
# send-message.sh against three real tmux-pane fixtures on a throwaway socket:
#
# 1. DELIVERED — a REPL that renders a ` ` input box and submits on Enter
# (text scrolls to history, box clears) => exit 0 "✓ delivered".
# 2. UNCONFIRMED — a pane with NO locatable prompt glyph. This is the exact
# historical FALSE POSITIVE: pre-patch it printed "✓ delivered"
# exit 0; post-patch it MUST fail loud (exit 2, stderr
# "could not confirm submission").
# 3. DRAFT — a ` `-prompt pane that never submits (message stays on the
# input line) => exit 2, stderr "unsubmitted draft".
# 4. DELIVERED — a pane whose input box is two `─` rules with NO prompt glyph
# (box shape) anywhere (pi's shape) and which submits => exit 0. Pre-#1362
# the glyph probe could not see this box at all, so EVERY send
# to such a pane reported "may be UNDELIVERED" while landing.
# 5. DRAFT — the same glyphless box, holding our tail across every flush
# (box shape) Enter => exit 2, stderr "unsubmitted draft". Pre-#1362 this
# also reported unconfirmed, so the true state was invisible.
set -uo pipefail
# Compatibility entry point. Screen-verdict semantics were rejected by
# independent review and superseded by Jason's explicit transport-only ruling.
# Historical fixtures and verdicts remain in frozen r2/r3 review exports.
# Do not reinterpret their false confirmations as delivery successes.
set -euo pipefail
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
SEND="$HERE/send-message.sh"
SOCKET="verdict-test-$RANDOM-$$"
TMP=$(mktemp -d)
trap 'tmux -L "$SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; }
no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
command -v tmux >/dev/null 2>&1 || { echo "tmux required" >&2; exit 1; }
# --- Fixture 1: a submitting REPL with a prompt box (interactive bash, glyph PS1).
# readline strips bracketed-paste markers just like a real agent REPL; Enter
# executes (text -> scrollback), leaving a fresh empty ` ` box.
tmux -L "$SOCKET" new-session -d -s repl -c "$TMP" \
'PS1=" " exec bash --noprofile --norc -i'
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=repl" -m "verdict fixture one delivered ok" 2>"$TMP/e1"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: -prompt REPL that submits => exit 0 ✓ delivered"
else
no "delivered: -prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
fi
# --- Fixture 2: NO prompt glyph (default bash PS1). THE regression: pre-patch this
# was a silent false-positive "delivered"; post-patch it must be unconfirmed→exit 2.
tmux -L "$SOCKET" new-session -d -s noglyph -c "$TMP" \
'PS1="sh-noglyph$ " exec bash --noprofile --norc -i'
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); then
no "unconfirmed: glyphless pane must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "could not confirm submission" "$TMP/e2"; then
ok "unconfirmed: glyphless pane => exit 2 + 'could not confirm submission' (false-positive FIXED)"
else
no "unconfirmed: glyphless pane => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e2")]"
fi
fi
# --- Fixture 3: a box that never submits (sleep ignores stdin; TTY echo keeps the
# pasted tail sitting on the line) => draft => exit 2.
tmux -L "$SOCKET" new-session -d -s draft -c "$TMP" \
'printf " "; exec sleep infinity'
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=draft" -r 1 -m "verdict fixture three stuck unsubmitted draft" 2>"$TMP/e3"); then
no "draft: unsubmitted message must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "unsubmitted draft" "$TMP/e3"; then
ok "draft: stuck -line message => exit 2 + 'unsubmitted draft'"
else
no "draft: stuck -line message => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e3")]"
fi
fi
# --- Fixtures 4 and 5: a pi-shaped pane. The input box is two `─` rules with the
# text between them and NO prompt glyph anywhere, so the glyph probe alone can
# never locate it and every send reports "may be UNDELIVERED" (#1362). The
# renderer below is the shape, not the runtime: MODE=clear submits (box empties),
# MODE=keep leaves the text sitting in the box.
cat > "$TMP/pibox.sh" <<'PIBOX'
#!/usr/bin/env bash
MODE=${1:-clear}
RULE=$(printf '─%.0s' $(seq 1 60))
buf=""
draw() {
printf '\033[H\033[2J'
printf 'fixture output line\n\n'
printf '%s\n' "$RULE"
printf '%s\n' "$buf"
printf '%s\n' "$RULE"
printf '~/fixture (main)\n'
printf 'tok 0 model fixture\n'
}
draw
while IFS= read -r line; do
# keep: hold the tail across every flush Enter, which is what a stuck draft does.
if [ "$MODE" = keep ]; then [ -n "$line" ] && buf=$line; else buf=""; fi
draw
done
PIBOX
chmod +x "$TMP/pibox.sh"
tmux -L "$SOCKET" new-session -d -s pibox -c "$TMP" "exec bash '$TMP/pibox.sh' clear"
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=pibox" -m "pi fixture four delivered ok" 2>"$TMP/e4"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: glyphless box-drawn REPL that submits => exit 0 ✓ delivered"
else
no "delivered: glyphless box-drawn REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e4")]"
fi
tmux -L "$SOCKET" new-session -d -s piboxdraft -c "$TMP" "exec bash '$TMP/pibox.sh' keep"
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=piboxdraft" -r 1 -m "pi fixture five stuck in the box" 2>"$TMP/e5"); then
no "draft: glyphless box-drawn pane holding our tail must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "unsubmitted draft" "$TMP/e5"; then
ok "draft: message left in a glyphless box => exit 2 + 'unsubmitted draft'"
else
no "draft: message left in a glyphless box => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e5")]"
fi
fi
echo "---"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]
exec bash "$HERE/test-send-message-transport.sh"