fix(engine)!: identity is derived, never invented
Four defects that all reduce to the same thing: the engine trusted a name it had no business trusting. _broadcast is now a reserved namespace, not a repo. coord-send guarded retraction with a sender check, but that guard only covered the door it was nailed to: `coord-done --repo _broadcast <file>` walked in the side entrance and archived a broadcast out of the queue - a full unauthenticated retract of an announcement for every repo that had not read it yet. The rule reserves the whole `_` prefix rather than one literal, so a later `_seen` or `_config` cannot reopen the hole, and it is enforced in every CLI: a rule held in three of four places is not a rule. The pwd fallback is gone. It existed so the CLI would work anywhere, but "anywhere" includes every global surface: a session in ~/repos is not a repo, and basename(pwd) silently handed it the identity "repos". That is not hypothetical - mail was delivered under exactly that name. git toplevel or an explicit --from/--repo are now the only two sources. The write paths refuse and say so; the read path declines silently, because the hook runs it at every session start and must never fail a session. Two checkouts with the same directory name still share one mailbox - re-keying identity would break every existing mailbox and the readable `--to <repo>` addressing. Instead the first git-derived read records the claiming path in <repo>/.origin, and a read from elsewhere is warned about in the injection. A warning, not a refusal: the same repo moved or re-cloned is the ordinary case. The warning goes in the injection because the hook discards stderr, and a warning nobody can see is not a warning. The collision is live in this tree: claude-code-100x is nested inside a repo of the same name. Broadcast delivery is recorded only after the injection is written. Marking inside the read loop left a window where the seen set said "delivered" while the operator saw nothing, and the hook runs under `timeout: 10`, so the window was reachable. A lost broadcast is unrecoverable by design - the seen set is delivery history and retraction deliberately leaves it alone - so the failure mode has to be redelivery, never loss. The hook stops resolving identity altogether. It was the fourth copy of the rule and the only one that runs in production, so passing --repo bypassed the engine's guards exactly where they mattered and suppressed the collision check along with them. It is now the pure wrapper the boundary rule always claimed it was, pinned by two behavioral tests rather than by reading the source. Selftest 93 -> 116; three node tests cover the hook. BREAKING CHANGE: coord-send and coord-done exit 2 outside a git repo instead of naming themselves after the working directory. Pass --from/--repo to choose an identity explicitly. Repo names beginning with _ are refused everywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
This commit is contained in:
parent
737127a14c
commit
1d62b073f8
11 changed files with 1195 additions and 28 deletions
10
README.md
10
README.md
|
|
@ -34,7 +34,9 @@ Mailbox layout (default `~/.claude/coord/`, override with `CLAUDE_COORD_DIR`):
|
|||
_broadcast/archive/ retracted broadcasts (kept, never deleted)
|
||||
_broadcast/seen/<repo> per-repo seen set: delivered broadcast filenames, one per line
|
||||
|
||||
Repo identity is the basename of the git toplevel (fallback: the working directory). There is no registration — a repo joins the moment something is sent to it, or when it first reads a broadcast.
|
||||
Repo identity is the basename of the git toplevel, or an explicit `--from`/`--repo`. There is no third source: a directory that is not a git repo has no identity, so the write paths refuse and the read path stays silent. (The old fallback to the working-directory name was removed in 0.6.0 — on a global surface like `~/repos` it invented the identity `repo`-shaped names that belong to nobody, and signed real mail with them.) There is no registration — a repo joins the moment something is sent to it, or when it first reads a broadcast.
|
||||
|
||||
Because identity is a *basename*, two checkouts with the same directory name share one mailbox. The first git-derived read records the claiming path in `<repo>/.origin`, and a read from a different path is warned about in the injection. It is a warning rather than a refusal: the same repo moved or re-cloned is the ordinary case. Names beginning with `_` are reserved for engine internals (`_broadcast`) and are refused as repo identities everywhere.
|
||||
|
||||
Message format (filename `<UTC-timestamp>-<uniq>-from-<sender>.md`):
|
||||
|
||||
|
|
@ -85,7 +87,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.
|
||||
|
||||
Every guarantee above is pinned by the 93-check selftest, including forgery-resistance regressions.
|
||||
Every guarantee above is pinned by the 116-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.
|
||||
|
||||
|
|
@ -103,11 +105,11 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
|
|||
|
||||
- macOS or Linux with bash 3.2+ (the scripts are deliberately bash-3.2-safe and ASCII-only).
|
||||
- Node.js >= 18 for the SessionStart hook (zero npm dependencies).
|
||||
- `git` is optional — without it, repo identity falls back to the directory basename.
|
||||
- `git` is required to derive repo identity automatically. Without it, pass `--from`/`--repo` explicitly; the engine refuses to guess an identity from the working directory.
|
||||
|
||||
## Development
|
||||
|
||||
bash scripts/coord-selftest.sh # 93 checks against a throwaway mailbox
|
||||
bash scripts/coord-selftest.sh # 116 checks against a throwaway mailbox
|
||||
npm test # same selftest via node --test
|
||||
|
||||
TDD is the house rule: every behavior change lands with a failing selftest check first.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue