--to is the only line-oriented field sanitize_field never covered, and the
fix is a refusal rather than a sanitize pass because --to is also the
destination DIRECTORY name ($COORD/$TO/inbox, and $COORD/$TO/orders in
coord-order-send.sh). Collapsing a newline to a space would deliver the
message to a mailbox the sender never named - the same misdelivery the
retired ktg-plugin-marketplace address is rejected rather than redirected
to avoid.
Both corruptions were measured on the live engine first, each with exit 0
and a "delivered" line:
--to "x\nreply-expected: no" the injected line lands INSIDE the
frontmatter block, above the reply-expected: yes the engine itself
wrote, so coord-count reads owed=0 and the declared debt is silenced -
defeating coord-count's own rule that only the frontmatter block may
speak, since the injected line IS in the block.
--to "tabbed<TAB>repo" coord-count prints five tab-separated fields
where its contract is four, so a consumer reads the mailbox name as the
part before the tab and the pending count as the part after.
board.sh consumes that TSV, so both reach the board.
The guard sits after reply-mode resolution: --reply-to takes its target from
the original's from: line, untrusted cross-repo input, and that is the one
target name nobody typed.
Denominator measured rather than assumed: two scripts build a directory from
a caller-supplied name, and coord-order-send.sh had the identical defect,
where it costs more - an order filed under a name no session can hold is the
silent evaporation the ownership chain exists to prevent, while board.sh's
ORDRE column counts the intended repo's queue and stays 0 with nothing
reporting a failure. The read-side --repo arguments resolve an EXISTING
directory, so a control character there finds nothing and writes nothing;
checked and left alone.
Closes finding 7 of docs/2026-08-14-confident-zero-review.md.
Tests (red first, both suites): coord-selftest section 35 (10 checks) and
orders-selftest section 10 (6 checks), each with the mandatory
known-positive controls - an ordinary name still delivers, and so does a
dot-prefixed one, since a dot name is a real repo. coord 230/230, board
252/252, route 69/69, orders 110/110, guard 40/40, npm test 11/11.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgKcURiXwGKhqCKhwD1NAp
305 lines
16 KiB
Bash
Executable file
305 lines
16 KiB
Bash
Executable file
#!/bin/bash
|
|
# coord-send.sh - deliver an inter-repo coordination message into the local
|
|
# mailbox (~/.claude/coord). Model-invoked; no network, no external service.
|
|
#
|
|
# Usage:
|
|
# 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.
|
|
# --fyi marks the message as expecting no reply (frontmatter reply-expected: no).
|
|
# Absent, a reply IS expected: every message written before the field existed
|
|
# lacks it, so absence has to keep meaning what it always meant, and a
|
|
# forgotten flag then over-counts debt instead of hiding it. The receiver is
|
|
# still free to close either kind with coord-done - the field declares what the
|
|
# sender expects, it does not oblige anyone. A broadcast is always
|
|
# reply-expected: no, because --reply-to resolves inside the recipient own
|
|
# mailbox and a broadcast never lands there: there is no reply path to promise.
|
|
# --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, 1 delivered but --reply-to's original could NOT be closed
|
|
# (the reply is sent; do not re-send it, close the original by hand),
|
|
# 2 usage/IO error, nothing written. ASCII only, bash 3.2 safe.
|
|
set -u
|
|
export LC_ALL=C
|
|
|
|
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
|
|
|
|
TO=""; BROADCAST=0; SUBJECT=""; FROM=""; MESSAGE=""; HAVE_MESSAGE=0; REPLYTO=""; REPLY_ORIG=""
|
|
RETRACT=""; FYI=0
|
|
|
|
# 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.
|
|
require_value() {
|
|
if [ "$2" -lt 2 ]; then echo "coord-send: $1 requires a value" >&2; exit 2; fi
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--to) require_value --to $#; TO="$2"; shift 2 ;;
|
|
--broadcast) BROADCAST=1; shift ;;
|
|
--fyi) FYI=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 ;;
|
|
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
*) echo "coord-send: unknown argument: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# --- Resolve sender / self identity ---
|
|
# git toplevel or an explicit --from, and nothing else. basename(pwd) used to
|
|
# be the last resort, but every global surface (~/repos, $HOME) is a directory
|
|
# without a repo, and the fallback quietly handed one an identity like "repos" -
|
|
# a real message was delivered under exactly that name. An invented identity is
|
|
# worse than none: it signs mail as a repo that does not exist and, on the read
|
|
# side, opens a mailbox that may belong to someone else. Refuse and say how.
|
|
if [ -z "$FROM" ]; then
|
|
FROM="$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)"
|
|
fi
|
|
if [ -z "$FROM" ]; then
|
|
echo "coord-send: cannot resolve sender identity (not inside a git repo); pass --from <repo> to choose one explicitly" >&2
|
|
exit 2
|
|
fi
|
|
# A leading _ is reserved for engine internals (_broadcast today; the rule
|
|
# reserves the namespace so a later _seen or _config cannot reopen the hole).
|
|
case "$FROM" in
|
|
_*) echo "coord-send: invalid sender identity: $FROM (names starting with _ are reserved for the engine)" >&2; exit 2 ;;
|
|
esac
|
|
|
|
# Frontmatter is line-oriented: a CR/LF inside a field would inject extra
|
|
# frontmatter lines or a premature '---' terminator. Collapse newlines to
|
|
# spaces and drop remaining control characters. Send-side input hygiene;
|
|
# the read side independently treats all content as untrusted.
|
|
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
|
|
echo "coord-send: --reply-to cannot be combined with --to/--broadcast" >&2; exit 2
|
|
fi
|
|
case "$REPLYTO" in */*|.|..|"") echo "coord-send: invalid --reply-to name: $REPLYTO" >&2; exit 2 ;; esac
|
|
REPLY_ORIG="$COORD/$FROM/inbox/$REPLYTO"
|
|
[ -e "$REPLY_ORIG" ] || REPLY_ORIG="$COORD/$FROM/archive/$REPLYTO"
|
|
if [ ! -e "$REPLY_ORIG" ]; then
|
|
echo "coord-send: --reply-to message not found for $FROM: $REPLYTO" >&2; exit 2
|
|
fi
|
|
TO="$(grep -m1 '^from:' "$REPLY_ORIG" 2>/dev/null | sed 's/^from:[[:space:]]*//')"
|
|
[ -z "$TO" ] && { echo "coord-send: cannot resolve sender of $REPLYTO" >&2; exit 2; }
|
|
if [ -z "$SUBJECT" ]; then
|
|
osub="$(grep -m1 '^subject:' "$REPLY_ORIG" 2>/dev/null | sed 's/^subject:[[:space:]]*//')"
|
|
SUBJECT="Re: $osub"
|
|
fi
|
|
fi
|
|
SUBJECT="$(sanitize_field "$SUBJECT")"
|
|
|
|
# --- Validate target: exactly one of --to / --broadcast (reply mode sets --to above) ---
|
|
if [ "$BROADCAST" -eq 1 ] && [ -n "$TO" ]; then
|
|
echo "coord-send: use either --to <repo> or --broadcast, not both" >&2; exit 2
|
|
fi
|
|
if [ "$BROADCAST" -eq 0 ] && [ -z "$TO" ]; then
|
|
echo "coord-send: missing --to <repo> / --broadcast / --reply-to" >&2; exit 2
|
|
fi
|
|
if [ "$BROADCAST" -eq 0 ]; then
|
|
# _* rather than the single literal _broadcast: the reserved namespace is a
|
|
# rule, so a future internal directory is covered the day it is added.
|
|
# Exact . and .. only, not a `.*` prefix match: a dot-prefixed name is a real
|
|
# repo (basename of a git toplevel under a hidden directory, e.g. ~/.claude),
|
|
# and the guard's job is to stop path traversal, not every hidden name.
|
|
case "$TO" in
|
|
*/*|.|..|_*) echo "coord-send: invalid target repo name: $TO" >&2; exit 2 ;;
|
|
esac
|
|
# --to is the one line-oriented field that is REFUSED rather than sanitized,
|
|
# and the asymmetry with FROM/SUBJECT above is deliberate: --to is also the
|
|
# destination DIRECTORY name ("$COORD/$TO/inbox"). Collapsing a newline to a
|
|
# space would deliver the message to a mailbox the sender never named, which
|
|
# is the same misdelivery the retired ktg-plugin-marketplace address is
|
|
# rejected rather than redirected to avoid. Measured before this guard
|
|
# existed, both with exit 0 and a "delivered" line: a newline injects its
|
|
# payload INSIDE the frontmatter block (silencing the reply-expected: yes the
|
|
# engine itself wrote, since coord-count reads the first match), and a tab
|
|
# gives coord-count five tab-separated fields where its contract is four, so
|
|
# a consumer reads the mailbox name and the pending count off by one column.
|
|
# board.sh consumes that TSV. Reply mode resolves TO from the original's
|
|
# from: line - untrusted cross-repo input - so this must sit AFTER that
|
|
# resolution, covering the one target name nobody typed.
|
|
case "$TO" in
|
|
*[[:cntrl:]]*)
|
|
echo "coord-send: --to contains a control character: $(sanitize_field "$TO") (a target name is also the mailbox directory name, so it is refused, never sanitized)" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
# Retired address (operator decision 2026-08-15, catalog's H4 reply
|
|
# archived 2026-08-15T16:27:51Z): ktg-plugin-marketplace is a polyrepo
|
|
# DIRECTORY, not a git repo, so basename(git toplevel) can never resolve to
|
|
# it and no session was ever able to hold this identity naturally. REJECT,
|
|
# not a silent redirect to catalog - a redirect delivers mail somewhere the
|
|
# sender does not believe it landed, which is the same misdelivery defect
|
|
# this closes (2 messages sat undelivered 2 days on this exact
|
|
# misaddressing before catalog's H4 count caught it). Only --to is retired;
|
|
# --from is untouched, since the defect was mail ARRIVING here, not mail
|
|
# claiming to originate here.
|
|
case "$TO" in
|
|
ktg-plugin-marketplace)
|
|
echo "coord-send: ktg-plugin-marketplace is a retired coord address (it is a polyrepo directory, not a git repo - no session can ever hold that identity); send to --to catalog instead" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
fi
|
|
if [ -z "$SUBJECT" ]; then
|
|
echo "coord-send: missing --subject" >&2; exit 2
|
|
fi
|
|
|
|
# --- Body: --message or stdin ---
|
|
if [ "$HAVE_MESSAGE" -eq 1 ]; then BODY="$MESSAGE"; else BODY="$(cat)"; fi
|
|
if [ -z "$BODY" ]; then
|
|
echo "coord-send: empty message body (pass --message or pipe text on stdin)" >&2; exit 2
|
|
fi
|
|
|
|
# --- Resolve destination ---
|
|
if [ "$BROADCAST" -eq 1 ]; then
|
|
TARGET_LABEL="broadcast"; DEST_DIR="$COORD/_broadcast/inbox"
|
|
else
|
|
TARGET_LABEL="$TO"; DEST_DIR="$COORD/$TO/inbox"
|
|
fi
|
|
mkdir -p "$DEST_DIR" 2>/dev/null || { echo "coord-send: cannot create $DEST_DIR" >&2; exit 2; }
|
|
|
|
# The filename must be shell-clean: it round-trips through coord-inbox's
|
|
# reply/resolve hints and coord-done's positional args. Sanitize the sender
|
|
# for the filename only; the raw name stays in the from: frontmatter.
|
|
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
SAFE_FROM="$(printf '%s' "$FROM" | tr -c 'A-Za-z0-9._-' '-')"
|
|
FNAME="${TS}-$$${RANDOM}-from-${SAFE_FROM}.md"
|
|
DEST="$DEST_DIR/$FNAME"
|
|
DATE_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
# Temp file lives INSIDE the destination dir (dot-prefixed so the inbox
|
|
# *.md glob never sees it): the final mv is then a same-filesystem rename,
|
|
# so readers never observe a half-written message.
|
|
REPLY_EXPECTED=yes
|
|
[ "$FYI" -eq 1 ] && REPLY_EXPECTED=no
|
|
[ "$BROADCAST" -eq 1 ] && REPLY_EXPECTED=no
|
|
|
|
TMP="$(mktemp "$DEST_DIR/.coord-send.XXXXXX" 2>/dev/null)"
|
|
[ -n "$TMP" ] || { echo "coord-send: cannot create temp file in $DEST_DIR" >&2; exit 2; }
|
|
{
|
|
echo "---"
|
|
echo "from: $FROM"
|
|
echo "to: $TARGET_LABEL"
|
|
echo "subject: $SUBJECT"
|
|
echo "date: $DATE_ISO"
|
|
# Fifth field, appended after the historic four so an older reader that stops
|
|
# at the ones it knows is unaffected. A broadcast is pinned to "no" whether or
|
|
# not --fyi was passed: that is not a defaulted value but the absence of a
|
|
# reply path (see the header), and a message that claimed otherwise would be
|
|
# asking for something the engine cannot deliver.
|
|
echo "reply-expected: $REPLY_EXPECTED"
|
|
echo "---"
|
|
printf '%s\n' "$BODY"
|
|
} > "$TMP"
|
|
if ! mv "$TMP" "$DEST" 2>/dev/null; then
|
|
/bin/rm -f "$TMP" 2>/dev/null; echo "coord-send: write failed" >&2; exit 2
|
|
fi
|
|
|
|
echo "coord-send: delivered to $TARGET_LABEL ($FNAME)"
|
|
|
|
# A broadcast reaches every repo INCLUDING the sender, so mark it seen for the
|
|
# sender now: the announcing repo already knows its own news, and re-injecting
|
|
# it would burn operator attention at every session start. Keyed by the RAW
|
|
# sender name, because coord-inbox keys the seen file by the unsanitized repo
|
|
# name - using SAFE_FROM here would silently miss for names like "my repo".
|
|
# Raw means it must be a safe path component: skip rather than escape the dir.
|
|
if [ "$BROADCAST" -eq 1 ]; then
|
|
case "$FROM" in
|
|
*/*|.|..) : ;;
|
|
*) SEEN_DIR="$COORD/_broadcast/seen"
|
|
mkdir -p "$SEEN_DIR" 2>/dev/null &&
|
|
printf '%s\n' "$FNAME" >> "$SEEN_DIR/$FROM" 2>/dev/null ;;
|
|
esac
|
|
fi
|
|
|
|
# --- Reply mode: mark the original handled ---
|
|
# The handled claim is asserted against GROUND TRUTH - is the original still
|
|
# pending in the inbox - and not against the call having been made. It used to
|
|
# print unconditionally with coord-done's output discarded, which made the one
|
|
# line a session relies on to close a reply debt false at the moment it was
|
|
# printed (review finding 9, 2026-08-14: stub coord-done exiting 1, original
|
|
# untouched, no archive/, and coord-send still exited 0 saying "marked
|
|
# handled"). A false success in the transport is worse than a loud failure:
|
|
# every reply had to be verified by hand afterwards, so the exit code carried
|
|
# no information at all.
|
|
#
|
|
# CHECKING THE EXIT CODE ALONE IS NOT ENOUGH, and this is the half a later
|
|
# session is most likely to simplify away. coord-done exits 0 when it archives
|
|
# NOTHING - an unknown name is idempotently fine by its own contract
|
|
# (coord-done.sh:54, :70) - so a nonzero-exit test still certifies a message
|
|
# that never moved. Selftest section 34(b) is that exact case.
|
|
#
|
|
# The path is recomputed rather than reusing $REPLY_ORIG, which resolves to the
|
|
# inbox OR the archive (:129-130). Replying to an already-archived original is
|
|
# legitimate and moves nothing; testing $REPLY_ORIG would warn on every one of
|
|
# those (section 34(d)).
|
|
#
|
|
# Exit 1, not 2: the reply WAS delivered and re-sending it would duplicate it.
|
|
# The distinct status says "delivered, original not closed" - 2 stays the
|
|
# nothing-was-written status it has always been.
|
|
if [ -n "$REPLY_ORIG" ]; then
|
|
"$(dirname "$0")/coord-done.sh" --repo "$FROM" "$REPLYTO" >/dev/null 2>&1
|
|
DONE_RC=$?
|
|
if [ "$DONE_RC" -eq 0 ] && [ ! -e "$COORD/$FROM/inbox/$REPLYTO" ]; then
|
|
echo "coord-send: original ($REPLYTO) marked handled"
|
|
else
|
|
echo "coord-send: the reply was delivered, but the original ($REPLYTO) is STILL PENDING in $FROM's inbox (coord-done exit $DONE_RC) - it is NOT handled; close it by hand: coord-done $REPLYTO" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
exit 0
|