feat(bridges): render agents under their display name, defused on every platform - #330
Open
raullopez-sandbox wants to merge 6 commits into
Open
feat(bridges): render agents under their display name, defused on every platform#330raullopez-sandbox wants to merge 6 commits into
raullopez-sandbox wants to merge 6 commits into
Conversation
raullopez-sandbox
requested review from
amaudruz and
christian-mcdermott
as code owners
September 1, 2026 13:19
raullopez-sandbox
marked this pull request as draft
September 1, 2026 15:28
raullopez-sandbox
force-pushed
the
feat/display-name-01-column
branch
from
September 2, 2026 07:24
8a15e25 to
4c6cf4c
Compare
…ry platform Agents are addressed by a lowercase identifier (`switchdev`). That name is also what every collaboration bridge showed to humans. This renders the presentation `display_name` instead, where one is set, without moving the identifier anywhere it is used for routing. Rendering lands on Slack and Discord here; Teams, Telegram and Mattermost follow. The escaping lands on all five at once, and has to: `_ping_operator` is on the base class, so the moment this commit exists every platform inlines a display name into a message body, including the three that do not otherwise render one yet. Splitting the escaping across the later commits would leave main exposed in between. The identifier stays load-bearing and untouched. `clients.display_name` is still the Matrix identity stamped onto every event as `sender_name`; icons are still derived from the identifier; and the echo filters that stop an agent re-importing its own message are unchanged — Slack matches on `bot_id`, Discord on `webhook_id`/`author.id`, both name-independent. Mattermost's name-based check is out of scope here and unaffected. Label and icon resolve together in one lookup. `bridge_core` installs an `AgentPresentation` resolver on the adapter; `agent_rendering(name)` returns an `AgentRendering` carrying the field label, the body-escaped label and the icon. Slack keeps its white-background icon recolouring by overriding the pure `adapt_icon_url` hook rather than the accessor, so the recolour applies everywhere that posts as an agent. `escape_label_for_body` is safe by default rather than empty by default. It defuses `@` and `]` with a zero-width space, which covers the two constructs a name can use to claim something it is not — addressing somebody, and rendering an anchor whose destination it chose. The `]` and not the `[`: the zero-width space has to break the `](` adjacency, and after the `[` it lands inside the visible text and the link still matches. A platform that adds no override is now safe, where before it rendered a forged name and said nothing. An override adds its platform's rule on top and calls up to the base one. None replaces it, and the `@` rule is why. A platform's escaper defends against the label carrying that platform's syntax — but Switch's own outbound mention pass runs over the finished body, after the label is inlined, and turns a plain `@handle` into a real mention needing no syntax from the label at all. Empirically, before this: a display name of `@opsbot` produced `<!subteam^S999>` on Slack and `<@&4242>` on Discord, and `@alice` produced `<@777>`. Both adapters escaped correctly; the defect was in the composition. On top of that base: Discord escapes markdown and mentions and inserts U+200B after `<`, since it resolves `<#…>`, `<t:…>` and `<:emoji:…>` out of raw content and has no backslash escape for `<`. Every Discord send also passes `AllowedMentions(everyone=False)`, so a mass ping is refused by permission as well as defused in text. Slack escapes `&`, `<` and `>`; mrkdwn has no escape for `*`, `_`, `~` or backtick, which the docstring states rather than pretending otherwise. Teams breaks `<at>`, which `_mention_entities` finds in a rendered body and pairs with a live entity. Field values are left unescaped deliberately — platforms render those verbatim. Emphasis is deliberately not covered anywhere. A label's `*` or `_` can unbalance the run around it; that is cosmetic, and the line held is that forged markup is closed while broken formatting is not worth an escape that would show up in every ordinary name. Discord may reject a display name as a webhook username with a 400. That falls back to the identifier and latches for the rest of the send, so a multi-call send cannot post half under each name. A bare URL in a display name still auto-links. Left as-is and documented: a display name is never dereferenced by the server, unlike `icon_url`, and an agent's owner can already have it post any link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from
feat/display-name-01-column
to
feat/display-name-00-console-slug-suggestion
September 2, 2026 07:38
raullopez-sandbox
force-pushed
the
feat/display-name-02-bridge-rendering
branch
from
September 2, 2026 07:45
6c28fe9 to
7537b0b
Compare
raullopez-sandbox
marked this pull request as ready for review
September 2, 2026 08:20
Base automatically changed from
feat/display-name-00-console-slug-suggestion
to
main
September 2, 2026 09:10
Resolved merge conflict in adapter.py by combining: - Display name rendering from the PR branch (using agent_label_for_body) - Error handling logic from main (detail parameter with reason/need/lead) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Resolved conflict in adapter.py by combining display name feature with error detail feature: - Uses agent_label_for_body to show display name - Supports detail parameter for error reasons - Shows warning emoji when there's an error Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The agent detail page header now shows the agent's display name as the primary title when available, falling back to the machine name. When both exist and differ, the machine name is shown as a subtitle in monospace. Changes: - Add displayName field to RemoteAgentSummary type - Update agent page header to prioritize displayName over machine name - Show machine name as subtitle when it differs from display name - Update test fixtures to include displayName field Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…b.com/sandbox-quantum/switch into feat/display-name-02-bridge-rendering
The agent list API endpoint was not returning the display_name field, causing the Switch Console UI to not show agent display names. Changes: - Add display_name field to AgentInfo schema - Update list_agents handler to include display_name in response - Update list_delegatable_agents to include display_name Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third PR in the display-name stack. Base is #325 — review that one first.
display_namecolumn has merged into it)feat(agents): store an optional human display name #326— thedisplay_namecolumn + API (merged)What changes
Agents are addressed by a lowercase identifier (
switchdev), and until now thatis also what humans saw. This shows the display name instead, where one is set.
Rendering lands on Slack and Discord here. The escaping lands on all five.
That split is deliberate:
_ping_operatoris on the base class, so the momentthis PR merges every platform inlines a display name into a message body —
including the three that do not otherwise render one yet. Leaving the escaping
to the later per-platform PRs would leave
mainexposed in between.What deliberately does not change
clients.display_nameis still the Matrix identity stamped onto every event assender_name. Breaking this re-imports an agent's own message as a third-party human — a puppet identity gets minted and the agent appears twice.bot_id, Discord onwebhook_id/author.id.PUT /bots/{id}at provisioning rather than a render change.How
bridge_coreinstalls anAgentPresentationresolver on the adapter, andagent_rendering(name)returns oneAgentRenderingcarrying the field label,the body-escaped label, and the icon — one DB lookup per send, where the
first draft would have done two.
Slack keeps its white-background icon recolouring by overriding the pure
adapt_icon_url(raw, name)hook rather than the accessor. This matters: Slack'sold override took a name and re-queried, so a naive paired accessor would have
silently bypassed the recolour.
Escaping: a safe default, specialised by override
escape_label_for_bodynow defuses@and]with a zero-width space insteadof returning the label unchanged. Those two cover the constructs a name can use
to claim something it is not: addressing somebody, and rendering an anchor whose
destination it chose and whose visible text hides it.
The
]and not the[— the zero-width space has to break the](adjacencyevery Markdown dialect requires. After the
[it lands inside the visible textand the link still matches; verified against the compiled patterns rather than
assumed.
A platform that adds no override is now safe. Before, it rendered a forged name
correctly and said nothing.
An override adds to the base rule; it does not replace it
A review flagged that Slack and Discord were still open, and its verifiers
refuted it. The finding was correct. Probing the real pipeline:
@opsbot@U123 *<!subteam^S999>* needs your input.@opsbot@123 **<@&4242>** needs your input.@alice@123 **<@777>** needs your input.Neither adapter is wrong on its own. A label never has to reach the platform in
the platform's syntax: Switch's own outbound mention pass runs over the
finished body, after the label is inlined, and turns a plain
@handleinto areal mention. The defect is in the composition of two individually-correct
functions — which is why reading either one refutes it. Every verifier checked
whether markup could survive the escaper; none checked what ran afterwards.
Both now call
super(). Three regression tests, each verified to fail withoutthe fix.
Per platform, on top of the base
<; Discord resolves<#…>,<t:…>and<:emoji:…>out of raw content and has no backslash escape for<. Every send also passesAllowedMentions(everyone=False), so a mass ping is refused by permission as well as defused in text.&,<,>. mrkdwn has no escape sequence for*,_,~or backtick; the docstring says so rather than implying coverage it does not have.<at>, which_mention_entitiesfinds in a rendered body and pairs with a live entity. Broken syntax rather than entity escaping: an Adaptive Card is not HTML, so<would be shown to the reader.Discord may reject a display name as a webhook username with a 400. That falls
back to the identifier and latches for the rest of the send, so a multi-call
send cannot post half under each name.
Known and disclosed, not fixed
*or_can unbalance the run around it. Cosmetic; the line held is that forged markup is closed while broken formatting is not worth an escape that would appear in every ordinary name.icon_url, which is hardened precisely because the server fetches it — and an agent's owner can already have it post any link.Verification
mainin a throwaway worktree: clean, 2195 pass. fix(slack): resolve agent mentions that crossed a workspace boundary #327's Slack change touches the inbound mention path, not the outbound one, so it does not reopen this.icon_url, un-escaping the body label, and removing eithersuper()call each make specific tests fail. One mutation passed, which exposed an accessor with zero production callers — since deleted.beforerebound to the actual prior implementation rather than a mock-up, and every row asserted to differ.Also in here: 22 test assertions contained literal invisible U+200B
characters, so
assert "@<U+200B>channel" in xsat directly aboveassert "@channel" not in xlooking like a contradiction. Converted to\u200bescapes; the tests still pass, confirming it was purely presentational.🤖 Generated with Claude Code