feat(engine): retire a broadcast with coord-send --retract
Nothing could remove a message from _broadcast/inbox/. coord-done is directed-only and never touches the broadcast queue, so the backlog could only grow: every new repo received the entire standing history at its first session, including announcements that had since become false. --retract <filename> archives the message into _broadcast/archive/, so no future repo is served it. Three deliberate limits, all pinned by tests: - Un-send, not recall. Repos that already received it keep it; _broadcast/seen/ is delivery history and is left untouched. - Only the sender may retract (from: must match the repo identity). --from overrides it, as everywhere else in the engine, which makes the check an accident guard rather than a security boundary. - Nothing is deleted, mirroring coord-done. Retracting twice is a no-op. The branch runs before every send-side validation and before the stdin body read, since a retract carries no subject and no body. Selftest 70 -> 82 (new section 19). Also fixes two README defects the feature exposed: the install command still named coord@ after the v0.3.0 rename, and the docs advised pruning _broadcast/inbox/ by hand, which contradicted the rule that the script owns mailbox files.
This commit is contained in:
parent
c27b20fc62
commit
316b8acdd2
8 changed files with 184 additions and 15 deletions
|
|
@ -235,6 +235,49 @@ printf '%s' "$aerr" | grep -q 'unknown argument: --bogus-flag'; check "inbox: un
|
|||
aout="$("$INBOX" --repo argrepo --bogus-flag 2>/dev/null)"; rc=$?
|
||||
[ "$rc" -eq 0 ] && printf '%s' "$aout" | grep -q 'ARG-BODY'; check "inbox: unknown argument still reads the inbox and exits 0" $?
|
||||
|
||||
# 19. A broadcast can be retired. Without this, the broadcast backlog grows
|
||||
# monotonically: every NEW repo receives the whole history at its first session,
|
||||
# including announcements that have since become false. Retract is sender-side
|
||||
# ("un-send"), archives rather than deletes, and is NOT recall - repos that
|
||||
# already received the message keep it. Every invocation reads from /dev/null so
|
||||
# a regression that falls through to the stdin body read fails instead of hanging.
|
||||
"$SEND" --broadcast --from retractor --subject "oops" --message "RETRACT-ME-BODY" >/dev/null
|
||||
rbase="$(basename "$(ls "$CLAUDE_COORD_DIR"/_broadcast/inbox/*-from-retractor.md 2>/dev/null | head -1)")"
|
||||
"$SEND" --broadcast --from keeper --subject "keep" --message "KEEP-BC-BODY" >/dev/null
|
||||
nr="$("$SEND" --retract "$rbase" --from someone-else </dev/null 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ] && printf '%s' "$nr" | grep -q 'sent by retractor'; check "retract by a non-sender is refused" $?
|
||||
[ -e "$CLAUDE_COORD_DIR/_broadcast/inbox/$rbase" ]; check "refused retract leaves the broadcast pending" $?
|
||||
vo="$("$SEND" --retract </dev/null 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ] && printf '%s' "$vo" | grep -q 'requires a value'; check "retract: missing value is a usage error" $?
|
||||
uo="$("$SEND" --retract no-such-broadcast.md --from retractor </dev/null 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 2 ] && printf '%s' "$uo" | grep -q 'not found'; check "retract of an unknown broadcast exits 2" $?
|
||||
bad_rc=0
|
||||
for bad in . .. sub/dir; do
|
||||
bo="$("$SEND" --retract "$bad" --from retractor </dev/null 2>&1)"; brc=$?
|
||||
{ [ "$brc" -eq 2 ] && printf '%s' "$bo" | grep -q 'invalid --retract'; } || bad_rc=1
|
||||
done
|
||||
[ "$bad_rc" -eq 0 ]; check "retract rejects . / .. / path names as invalid" $?
|
||||
comb_rc=0
|
||||
for combo in --broadcast "--to:x" "--reply-to:y"; do
|
||||
cflag="${combo%%:*}"; cval="${combo#*:}"
|
||||
if [ "$cval" = "$combo" ]; then
|
||||
co="$("$SEND" --retract "$rbase" "$cflag" --from retractor </dev/null 2>&1)"; crc=$?
|
||||
else
|
||||
co="$("$SEND" --retract "$rbase" "$cflag" "$cval" --from retractor </dev/null 2>&1)"; crc=$?
|
||||
fi
|
||||
{ [ "$crc" -eq 2 ] && printf '%s' "$co" | grep -q 'cannot be combined'; } || comb_rc=1
|
||||
done
|
||||
[ "$comb_rc" -eq 0 ]; check "retract cannot be combined with --to/--broadcast/--reply-to" $?
|
||||
ro="$("$SEND" --retract "$rbase" --from retractor </dev/null 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 0 ]; check "retract by the sender exits 0" $?
|
||||
[ ! -e "$CLAUDE_COORD_DIR/_broadcast/inbox/$rbase" ]; check "retracted broadcast leaves the broadcast inbox" $?
|
||||
[ -e "$CLAUDE_COORD_DIR/_broadcast/archive/$rbase" ]; check "retracted broadcast is archived, never deleted" $?
|
||||
fout="$("$INBOX" --repo fresh-reader)"
|
||||
[ "$(printf '%s' "$fout" | grep -c 'RETRACT-ME-BODY')" -eq 0 ]; check "retracted broadcast is never delivered to a new repo" $?
|
||||
printf '%s' "$fout" | grep -q 'KEEP-BC-BODY'; check "unretracted broadcasts still reach that repo" $?
|
||||
io="$("$SEND" --retract "$rbase" --from retractor </dev/null 2>&1)"; rc=$?
|
||||
[ "$rc" -eq 0 ] && printf '%s' "$io" | grep -q 'already retracted'; check "re-retracting an already retracted broadcast is a no-op (exit 0)" $?
|
||||
|
||||
echo "----"
|
||||
echo "PASS=$PASS FAIL=$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@
|
|||
# coord-send.sh --to <repo> --subject "<subject>" [--from <repo>] [--message "<text>"]
|
||||
# coord-send.sh --broadcast --subject "<subject>" [--from <repo>] [--message "<text>"]
|
||||
# coord-send.sh --reply-to <file> [--subject "Re: ..."] [--from <repo>] [--message "<text>"]
|
||||
# coord-send.sh --retract <file> [--from <repo>]
|
||||
# Body comes from --message, or from stdin (heredoc) when --message is omitted.
|
||||
# --reply-to <basename> replies to a message in THIS repo's inbox/archive: it
|
||||
# routes to the original sender and marks the original handled (coord-done).
|
||||
# --retract <basename> retires one of YOUR OWN broadcasts: it is archived out of
|
||||
# the broadcast queue so no future repo receives it. This is un-send, not
|
||||
# recall - repos that already received it are unaffected.
|
||||
# --from overrides the sender/self identity (default: basename of git toplevel/cwd).
|
||||
#
|
||||
# Exit: 0 delivered, 2 usage/IO error. ASCII only, bash 3.2 safe.
|
||||
|
|
@ -18,6 +22,7 @@ export LC_ALL=C
|
|||
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
|
||||
|
||||
TO=""; BROADCAST=0; SUBJECT=""; FROM=""; MESSAGE=""; HAVE_MESSAGE=0; REPLYTO=""; REPLY_ORIG=""
|
||||
RETRACT=""
|
||||
|
||||
# bash 3.2: `shift 2` past the end of $# is a no-op, so a trailing value-flag
|
||||
# without its value would loop forever. Every two-arg flag must check first.
|
||||
|
|
@ -30,6 +35,7 @@ while [ $# -gt 0 ]; do
|
|||
--to) require_value --to $#; TO="$2"; shift 2 ;;
|
||||
--broadcast) BROADCAST=1; shift ;;
|
||||
--reply-to) require_value --reply-to $#; REPLYTO="$2"; shift 2 ;;
|
||||
--retract) require_value --retract $#; RETRACT="$2"; shift 2 ;;
|
||||
--subject) require_value --subject $#; SUBJECT="$2"; shift 2 ;;
|
||||
--from) require_value --from $#; FROM="$2"; shift 2 ;;
|
||||
--message) require_value --message $#; MESSAGE="$2"; HAVE_MESSAGE=1; shift 2 ;;
|
||||
|
|
@ -52,6 +58,46 @@ fi
|
|||
sanitize_field() { printf '%s' "$1" | tr '\r\n' ' ' | tr -d '\000-\037'; }
|
||||
FROM="$(sanitize_field "$FROM")"
|
||||
|
||||
# --- Retract mode: retire one of our own broadcasts ---
|
||||
# A broadcast has no per-repo owner that can close it: coord-done is directed-only
|
||||
# and never touches _broadcast/. Without this branch a stale announcement stays in
|
||||
# the queue and is delivered to every FUTURE repo forever, so the backlog can only
|
||||
# grow. Runs before every send-side validation and before the stdin body read: a
|
||||
# retract carries no subject and no body.
|
||||
# The sender check is an accident guard, not a security boundary - --from
|
||||
# redefines identity here exactly as it does everywhere else in this script. It
|
||||
# exists so a wrong filename cannot silently retire another repo's announcement.
|
||||
if [ -n "$RETRACT" ]; then
|
||||
if [ "$BROADCAST" -eq 1 ] || [ -n "$TO" ] || [ -n "$REPLYTO" ]; then
|
||||
echo "coord-send: --retract cannot be combined with --to/--broadcast/--reply-to" >&2; exit 2
|
||||
fi
|
||||
case "$RETRACT" in */*|.|..|"") echo "coord-send: invalid --retract name: $RETRACT" >&2; exit 2 ;; esac
|
||||
BC_INBOX="$COORD/_broadcast/inbox"
|
||||
BC_ARCHIVE="$COORD/_broadcast/archive"
|
||||
if [ ! -e "$BC_INBOX/$RETRACT" ]; then
|
||||
# Idempotent like coord-done: retracting twice is a no-op, not an error.
|
||||
if [ -e "$BC_ARCHIVE/$RETRACT" ]; then
|
||||
echo "coord-send: broadcast already retracted ($RETRACT)"; exit 0
|
||||
fi
|
||||
echo "coord-send: --retract broadcast not found: $RETRACT" >&2; exit 2
|
||||
fi
|
||||
ORIG_FROM="$(grep -m1 '^from:' "$BC_INBOX/$RETRACT" 2>/dev/null | sed 's/^from:[[:space:]]*//')"
|
||||
if [ "$ORIG_FROM" != "$FROM" ]; then
|
||||
echo "coord-send: refusing to retract a broadcast sent by ${ORIG_FROM:-unknown} (you are $FROM); pass --from ${ORIG_FROM:-<sender>} if that is really you" >&2
|
||||
exit 2
|
||||
fi
|
||||
# Archived, never deleted (coord-done's rule), and the move stays inside
|
||||
# _broadcast/ so it is a same-filesystem rename: no half-retracted state.
|
||||
mkdir -p "$BC_ARCHIVE" 2>/dev/null || { echo "coord-send: cannot create $BC_ARCHIVE" >&2; exit 2; }
|
||||
if ! mv "$BC_INBOX/$RETRACT" "$BC_ARCHIVE/$RETRACT" 2>/dev/null; then
|
||||
echo "coord-send: retract failed for $RETRACT" >&2; exit 2
|
||||
fi
|
||||
# _broadcast/seen/* is deliberately left alone: those entries are delivery
|
||||
# history, and one naming an archived file is inert.
|
||||
echo "coord-send: retracted broadcast ($RETRACT); repos that already received it are unaffected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Reply mode: resolve target + default subject from the original message ---
|
||||
if [ -n "$REPLYTO" ]; then
|
||||
if [ "$BROADCAST" -eq 1 ] || [ -n "$TO" ]; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue