feat(mentions): naming someone in a reply reaches them #458

Merged
coilyco-ops merged 1 commit from feat/naming-someone-reaches-them into main 2026-08-13 13:55:28 +00:00
Member

closes #457

The Echo half of #219. Kai activated the intent and said "Lets turn on the feature" at 09:02Z.

Two defects, each sufficient on its own

  • every reply carried Parse: [] — Discord parsed no mentions, so even a correct <@id> arrived inert
  • nothing anywhere resolved a name to an account

The service could name people and could not reach them. Thirteen hours of discussion treated this as an MCP grant; the Deep half is, and the Echo half never was.

Parse stays empty

The allowance is an explicit list of ids the harness resolved. A mention is something the harness decided to deliver, not something the model wrote — parsing reply text would let a model be talked into pinging @everyone, which is what the suppression was there for.

The roster is the conversation

An author in the history, the member who spoke, or someone one of those messages mentioned. No membership lookup, no API call — it is already in the payloads the turn was built from.

Every guild member and named roles were both considered. This is the narrowest: the only people reachable are people already in the room, and widening later is a change of source rather than of shape. I took that decision rather than escalating it, because the narrow version needs nothing new and answers the case the issue was filed about.

Four bounds, each because the alternative is worse than the feature

  • a name under three characters never resolves — it matches too much ordinary prose
  • a name inside a longer word is not that person; alphabet does not reach alpha
  • someone named four times is reached once — four pings for one sentence is what makes people mute a channel
  • an existing mention is left alone, and a longer name wins over a shorter one it contains

An empty roster leaves a reply byte-identical, which is every turn until someone is named.

ward exec gate green: build, policy-check, vet, test, test-skips, pre-commit.

closes #457 The Echo half of https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/219. Kai activated the intent and said *"Lets turn on the feature"* at 09:02Z. ## Two defects, each sufficient on its own - every reply carried `Parse: []` — Discord parsed **no** mentions, so even a correct `<@id>` arrived inert - **nothing anywhere resolved a name to an account** The service could name people and could not reach them. Thirteen hours of discussion treated this as an MCP grant; the Deep half is, and the Echo half never was. ## Parse stays empty The allowance is an explicit list of ids **the harness resolved**. A mention is something the harness decided to deliver, not something the model wrote — parsing reply text would let a model be talked into pinging `@everyone`, which is what the suppression was there for. ## The roster is the conversation An author in the history, the member who spoke, or someone one of those messages mentioned. **No membership lookup, no API call** — it is already in the payloads the turn was built from. Every guild member and named roles were both considered. This is the narrowest: the only people reachable are people already in the room, and widening later is a change of source rather than of shape. I took that decision rather than escalating it, because the narrow version needs nothing new and answers the case the issue was filed about. ## Four bounds, each because the alternative is worse than the feature - a name under three characters never resolves — it matches too much ordinary prose - a name inside a longer word is not that person; `alphabet` does not reach `alpha` - **someone named four times is reached once** — four pings for one sentence is what makes people mute a channel - an existing mention is left alone, and a longer name wins over a shorter one it contains An empty roster leaves a reply byte-identical, which is every turn until someone is named. `ward exec gate` green: build, policy-check, vet, test, test-skips, pre-commit.
feat(mentions): naming someone in a reply reaches them
All checks were successful
ci / image-build (pull_request) Successful in 19s
ci / test (pull_request) Successful in 32s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
452088d4ce
Every reply carried Parse empty, so Discord parsed no mentions and even a
correct id arrived as inert text. Nothing anywhere resolved a name to an
account. The service could name people and could not reach them.

Parse stays empty. The allowance is an explicit list of ids the harness
resolved, so a mention is something the harness decided to deliver rather than
something the model wrote. Parsing reply text would let a model be talked into
pinging everyone, which is what the suppression was there for.

The roster is the conversation: an author in the history, the member who spoke,
or someone one of those messages mentioned. That needs no membership lookup and
no API call, because it is already in the payloads the turn was built from, and
it is the narrowest of the plausible rosters. Widening to guild members or
roles is a change of source rather than of shape.

Four bounds, each because the alternative is worse than the feature. A name
under three characters never resolves. A name inside a longer word is not that
person. Someone named four times is reached once, since four pings for one
sentence is what makes people mute a channel. An existing mention is left
alone, and a longer name wins over a shorter one it contains.

closes #457

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Author
Member

Review — Angie (ENG) · s/4b1e. No objection. The safety property is preserved in the way that matters, and I want to say why it is the right way.

Parse stays empty and the allowance is an explicit Users list of ids the harness resolved. That keeps the original guarantee intact rather than trading it away: a mention is now something the harness decided to deliver, never something the model wrote. Parsing reply text would have been the obvious implementation and would have handed an attacker a route to a channel-wide ping, which is exactly what the empty Parse was protecting against. Preserving a bound while removing the limitation it caused is harder than either dropping it or keeping the feature out.

The roster choice is the second good decision. Authors in history, the member who spoke, and people those messages mentioned — all already in payloads the turn was built from, so no membership lookup and no API call. It is also the narrowest plausible source, and you say that widening to guild members or roles is a change of source rather than of shape, which tells the next person the extension point without inviting them through it.

The four bounds each have a reason and the reasons are about people rather than code. Someone named four times being reached once, because four pings for one sentence is what makes people mute a channel, is the one I would have missed. A three-character floor and a word-boundary rule are the difference between a feature and a nuisance.

Checked, not assumed: the diff shows forty lines removed from internal/community/sandboxwiring_test.go, which is the wiring test that landed a few minutes ago. The merge result keeps it at forty lines, identical to main. Stale-branch artifact, not a revert.

Merges clean.

One thing worth a follow-up rather than a change here. The roster is built from the turn's payloads, which include member-authored text. A member can therefore influence who is mentionable by naming someone in their own message. That is correct for the feature — reaching a person the conversation is about is the point — and it is also a small surface worth knowing exists: the set of reachable accounts is member-influenced, even though the decision to reach one is the harness's. Nothing to fix today; worth stating in the doc so the next person widening the roster knows they are widening an attacker-influenceable set.

**Review — Angie (ENG) · s/4b1e. No objection. The safety property is preserved in the way that matters, and I want to say why it is the right way.** `Parse` stays empty and the allowance is an explicit `Users` list of ids the harness resolved. That keeps the original guarantee intact rather than trading it away: **a mention is now something the harness decided to deliver, never something the model wrote.** Parsing reply text would have been the obvious implementation and would have handed an attacker a route to a channel-wide ping, which is exactly what the empty `Parse` was protecting against. Preserving a bound while removing the limitation it caused is harder than either dropping it or keeping the feature out. **The roster choice is the second good decision.** Authors in history, the member who spoke, and people those messages mentioned — all already in payloads the turn was built from, so no membership lookup and no API call. It is also the narrowest plausible source, and you say that widening to guild members or roles is a change of source rather than of shape, which tells the next person the extension point without inviting them through it. **The four bounds each have a reason and the reasons are about people rather than code.** Someone named four times being reached once, because four pings for one sentence is what makes people mute a channel, is the one I would have missed. A three-character floor and a word-boundary rule are the difference between a feature and a nuisance. **Checked, not assumed:** the diff shows forty lines removed from `internal/community/sandboxwiring_test.go`, which is the wiring test that landed a few minutes ago. The merge result keeps it at forty lines, identical to `main`. Stale-branch artifact, not a revert. Merges clean. **One thing worth a follow-up rather than a change here.** The roster is built from the turn's payloads, which include member-authored text. A member can therefore influence who is mentionable by naming someone in their own message. That is correct for the feature — reaching a person the conversation is about is the point — and it is also a small surface worth knowing exists: the set of reachable accounts is member-influenced, even though the decision to reach one is the harness's. Nothing to fix today; worth stating in the doc so the next person widening the roster knows they are widening an attacker-influenceable set.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/sirens-echo!458
No description provided.