fix(coord-send): stop rejecting dot-prefixed repo names
coord-send.sh:154 guarded the --to target with a `.*` case arm, which rejects every leading-dot name instead of just `.` and `..`. A dot-prefixed name is a real repo identity (basename of a git toplevel under a hidden directory, e.g. ~/.claude) and was reported unreachable by morning-driver via coord message 20260809T103138Z. Narrowed the guard to reject exactly `.` and `..`, matching the equivalent guards already used elsewhere in this file and in coord-done.sh. coord-count.sh and coord-sweep.sh both enumerate the mailbox root with a bare "$COORD"/* glob, which by construction never matches a dot-prefixed directory - confirmed empirically before this change. Fixing only the send-side guard would have let a dot-prefixed mailbox receive mail that neither script could ever report or close on its grace window. Both now also glob "$COORD"/.[!.]* to reach hidden mailboxes without matching "." or "..". coord-selftest.sh: 183 -> 191 checks, section 30 covers the fix and both enumeration paths. Doc counts (README badge, CLAUDE.md) updated to match; the catalog's mirrored badge is untouched pending release. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CHmf1VfCvaYXamJxe5y6Vt
This commit is contained in:
parent
ddc978d4c1
commit
f425a11311
6 changed files with 57 additions and 8 deletions
|
|
@ -16,7 +16,7 @@ marketplace plugin. Three components, one boundary:
|
||||||
`coord-done.sh` archives, `coord-count.sh` counts without delivering,
|
`coord-done.sh` archives, `coord-count.sh` counts without delivering,
|
||||||
`coord-sweep.sh` closes the aged FYI backlog machine-wide.
|
`coord-sweep.sh` closes the aged FYI backlog machine-wide.
|
||||||
Everything is pinned by `coord-selftest.sh`
|
Everything is pinned by `coord-selftest.sh`
|
||||||
(183 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
(191 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
|
||||||
|
|
||||||
**`coord-sweep.sh` is the only path that closes a message with no human in
|
**`coord-sweep.sh` is the only path that closes a message with no human in
|
||||||
the loop, and every constraint on it follows from that.** It may close exactly
|
the loop, and every constraint on it follows from that.** It may close exactly
|
||||||
|
|
@ -286,7 +286,7 @@ obligations in another repo.
|
||||||
- Zero dependencies everywhere: bash + coreutils in the engine, `node:`
|
- Zero dependencies everywhere: bash + coreutils in the engine, `node:`
|
||||||
builtins only in hook and tests.
|
builtins only in hook and tests.
|
||||||
- TDD: no behavior change without a failing selftest check first.
|
- TDD: no behavior change without a failing selftest check first.
|
||||||
`bash scripts/coord-selftest.sh` must exit 0 (183/183),
|
`bash scripts/coord-selftest.sh` must exit 0 (191/191),
|
||||||
`bash scripts/board-selftest.sh` must exit 0 (142/142) and
|
`bash scripts/board-selftest.sh` must exit 0 (142/142) and
|
||||||
`bash scripts/route-selftest.sh` must exit 0 (73/73).
|
`bash scripts/route-selftest.sh` must exit 0 (73/73).
|
||||||
- English for all code, docs, and commit messages (public repo). Norwegian
|
- English for all code, docs, and commit messages (public repo). Norwegian
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ Session A in repo X leaves a message for repo Y; the next session in repo Y gets
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ Cross-repo message content is untrusted input by design:
|
||||||
|
|
||||||
- **Atomic delivery:** the temp file is created inside the destination directory (dot-prefixed, invisible to the inbox glob), so the final rename never crosses filesystems and readers never observe a half-written message.
|
- **Atomic delivery:** the temp file is created inside the destination directory (dot-prefixed, invisible to the inbox glob), so the final rename never crosses filesystems and readers never observe a half-written message.
|
||||||
|
|
||||||
Every guarantee above is pinned by the 183-check selftest, including forgery-resistance regressions.
|
Every guarantee above is pinned by the 191-check selftest, including forgery-resistance regressions.
|
||||||
|
|
||||||
Note that raising the inbox's priority (Rule 7) deliberately does **not** widen this boundary: the obligation is to *respond* to a message, never to *comply* with it. The injection framing states both halves, and the selftest pins them together so a future reword cannot keep the priority and drop the distinction.
|
Note that raising the inbox's priority (Rule 7) deliberately does **not** widen this boundary: the obligation is to *respond* to a message, never to *comply* with it. The injection framing states both halves, and the selftest pins them together so a future reword cannot keep the priority and drop the distinction.
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
bash scripts/coord-selftest.sh # 183 checks against a throwaway mailbox
|
bash scripts/coord-selftest.sh # 191 checks against a throwaway mailbox
|
||||||
bash scripts/board-selftest.sh # 142 checks against a throwaway repo tree
|
bash scripts/board-selftest.sh # 142 checks against a throwaway repo tree
|
||||||
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
|
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
|
||||||
npm test # all three selftests via node --test
|
npm test # all three selftests via node --test
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,12 @@ owes_reply() {
|
||||||
|
|
||||||
# Glob expansion under LC_ALL=C is already name-sorted. An unmatched glob
|
# Glob expansion under LC_ALL=C is already name-sorted. An unmatched glob
|
||||||
# expands to the literal pattern, which fails the -d test and is skipped.
|
# expands to the literal pattern, which fails the -d test and is skipped.
|
||||||
for d in "$COORD"/*; do
|
# Two patterns, not dotglob: a bare "$COORD"/* never matches a dot-prefixed
|
||||||
|
# directory (e.g. .claude, a real repo's mailbox), and dotglob would also hand
|
||||||
|
# back "." and ".." plus stray dotfiles like .DS_Store - both filtered here
|
||||||
|
# only by luck of also failing -d. ".[!.]*" matches exactly the hidden
|
||||||
|
# directories, excluding "." and "..".
|
||||||
|
for d in "$COORD"/* "$COORD"/.[!.]*; do
|
||||||
[ -d "$d" ] || continue
|
[ -d "$d" ] || continue
|
||||||
name="$(basename "$d")"
|
name="$(basename "$d")"
|
||||||
# Reserved engine namespace (_broadcast): storage, not a correspondent.
|
# Reserved engine namespace (_broadcast): storage, not a correspondent.
|
||||||
|
|
|
||||||
|
|
@ -821,6 +821,44 @@ CLAUDE_COORD_DIR="$EDIR" "$SWEEP" --days 2>/dev/null; [ $? -eq 2 ]
|
||||||
check "sweep: --days without a value is a usage error, not a silent default" $?
|
check "sweep: --days without a value is a usage error, not a silent default" $?
|
||||||
/bin/rm -rf "$SDIR" "$EDIR" 2>/dev/null
|
/bin/rm -rf "$SDIR" "$EDIR" 2>/dev/null
|
||||||
|
|
||||||
|
# 30. Dot-prefixed repo names are real repos - basename of a git toplevel under
|
||||||
|
# a hidden directory, e.g. ~/.claude - and coord-send.sh's target guard used to
|
||||||
|
# reject every leading-dot name via a bare `.*` case arm, not just the `.` and
|
||||||
|
# `..` it was written to stop (reported by morning-driver 2026-08-09: ~/.claude
|
||||||
|
# could not be addressed at all). The read side never had this bug -
|
||||||
|
# coord-inbox.sh and coord-done.sh both take --repo directly, no directory
|
||||||
|
# glob - but coord-count.sh and coord-sweep.sh enumerate the mailbox root with
|
||||||
|
# a bare "$COORD"/* glob, which by construction never matches a dot-prefixed
|
||||||
|
# directory. Fixing only the send-side guard would let .dotrepo receive mail
|
||||||
|
# that coord-count.sh could never report and coord-sweep.sh could never close
|
||||||
|
# on its grace window - worse than today's clean refusal.
|
||||||
|
DDIR="$(mktemp -d)"
|
||||||
|
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to .dotrepo --from d1 --subject "dot test" --message "DOT-BODY" >/dev/null
|
||||||
|
[ -n "$(ls "$DDIR/.dotrepo/inbox"/*.md 2>/dev/null)" ]; check "send: dot-prefixed repo name accepted" $?
|
||||||
|
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to . --from d1 --subject x --message y >/dev/null 2>&1
|
||||||
|
[ $? -eq 2 ]; check "send: bare . target still rejected" $?
|
||||||
|
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to .. --from d1 --subject x --message y >/dev/null 2>&1
|
||||||
|
[ $? -eq 2 ]; check "send: bare .. target still rejected" $?
|
||||||
|
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to "../evil" --from d1 --subject x --message y >/dev/null 2>&1
|
||||||
|
[ $? -eq 2 ]; check "send: path-traversal through a dot prefix still rejected" $?
|
||||||
|
|
||||||
|
dout="$(CLAUDE_COORD_DIR="$DDIR" "$COUNT" 2>/dev/null)"
|
||||||
|
printf '%s\n' "$dout" | grep -q "^\.dotrepo${TAB}1${TAB}1$"
|
||||||
|
check "count: sees a dot-prefixed mailbox instead of skipping it" $?
|
||||||
|
|
||||||
|
# sweep uses the same enumeration as coord-count.sh, so an aged FYI inside a
|
||||||
|
# dot-prefixed mailbox has to be reachable too.
|
||||||
|
CLAUDE_COORD_DIR="$DDIR" "$SEND" --to .dotrepo --from d2 --fyi --subject "dot fyi" --message "DOT-FYI" >/dev/null
|
||||||
|
age_it "$DDIR" .dotrepo DOT-FYI 20200101T000000Z
|
||||||
|
check "sweep fixture: dot-repo fyi aged" $?
|
||||||
|
CLAUDE_COORD_DIR="$DDIR" "$SWEEP" --write >/dev/null 2>&1
|
||||||
|
grep -rq '^DOT-FYI$' "$DDIR/.dotrepo/archive" 2>/dev/null
|
||||||
|
check "sweep: closes an aged fyi inside a dot-prefixed mailbox" $?
|
||||||
|
|
||||||
|
CLAUDE_COORD_DIR="$DDIR" "$DONE" --repo .dotrepo --all >/dev/null
|
||||||
|
[ -z "$(ls "$DDIR/.dotrepo/inbox"/*.md 2>/dev/null)" ]; check "done: drains a dot-prefixed repo's inbox directly (unaffected by the bug)" $?
|
||||||
|
/bin/rm -rf "$DDIR" 2>/dev/null
|
||||||
|
|
||||||
echo "----"
|
echo "----"
|
||||||
echo "PASS=$PASS FAIL=$FAIL"
|
echo "PASS=$PASS FAIL=$FAIL"
|
||||||
[ "$FAIL" -eq 0 ]
|
[ "$FAIL" -eq 0 ]
|
||||||
|
|
|
||||||
|
|
@ -150,8 +150,11 @@ fi
|
||||||
if [ "$BROADCAST" -eq 0 ]; then
|
if [ "$BROADCAST" -eq 0 ]; then
|
||||||
# _* rather than the single literal _broadcast: the reserved namespace is a
|
# _* rather than the single literal _broadcast: the reserved namespace is a
|
||||||
# rule, so a future internal directory is covered the day it is added.
|
# 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
|
case "$TO" in
|
||||||
*/*|.*|_*) echo "coord-send: invalid target repo name: $TO" >&2; exit 2 ;;
|
*/*|.|..|_*) echo "coord-send: invalid target repo name: $TO" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
if [ -z "$SUBJECT" ]; then
|
if [ -z "$SUBJECT" ]; then
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,10 @@ field() {
|
||||||
}
|
}
|
||||||
|
|
||||||
CLOSED=0
|
CLOSED=0
|
||||||
for d in "$COORD"/*; do
|
# Two patterns, not dotglob: see the identical note in coord-count.sh, which
|
||||||
|
# enumerates the same mailbox root the same way. ".[!.]*" reaches dot-prefixed
|
||||||
|
# mailboxes (e.g. .claude) without also matching "." or "..".
|
||||||
|
for d in "$COORD"/* "$COORD"/.[!.]*; do
|
||||||
[ -d "$d" ] || continue
|
[ -d "$d" ] || continue
|
||||||
name="$(basename "$d")"
|
name="$(basename "$d")"
|
||||||
# Reserved engine namespace. _broadcast is storage, not a correspondent, and
|
# Reserved engine namespace. _broadcast is storage, not a correspondent, and
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue