Skip to content

host: four peer-name bounds still cut on UTF-16 units — published name, member line, room label, room-share envelope #1923

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: ff8983e4bac246018ebec8223f15e611
Branch: dev/1-published-name-cut
Base: ac81a4f
At: 2026-10-31T10:43:48Z

This block is FlyBot Lead's own claim record for this issue — it is not a third-party claim.

Dedupe (checked before opening): issue search for cleanPublishedName returns 0 results; room-share clip returns 7 issues, all closed except #1916 (merged by #1917, the host text cuts) and none about these four call sites; published name hits are older sanitising/identity issues (#75/#87) that do not touch these lines. Cross-checked at live main ac81a4f1 that no open issue names hooks.js:47, tools.js:84, reach.js:157 or shared/room-share.js:13.

Problem

#1916/#1917 replaced the host's code-unit cuts with cut() from core/src/shared/text-boundary.js, and #1918/#1921 is closing the memory-review pair. Four more display-name bounds still call .slice() on UTF-16 units, and they are the ones whose text crosses an owner boundary (a friend's Bot name into our roster; our names into a room envelope):

  • core/src/host/hooks.js:47 — cleanPublishedName ends with .trim().slice(0, 60); called on bot.name, bot.homeName and bot.owner at :947-949, and the result goes into state.flybot.remote.
  • core/src/host/tools.js:84 — the group member line: displayName .slice(0, 60), ownerName .slice(0, 40). This line reaches the model.
  • core/src/host/reach.js:157 — label ends with .trim().slice(0, 60); the labels are rendered by othersInRoom (:166/:167).
  • core/src/shared/room-share.js:13-14 — local clip(value, max = 60) ends with .trim().slice(0, max). Every roster entry, chat line and invite field goes through it (:107-:112, :120-:158, :165, :178), i.e. our own names as other owners' Bots receive them.

cut() already exists on main (core/src/shared/text-boundary.js), is exported as cut(text, max) => ({ text, cut }), and is imported by core/src/host/text.js:4 via '../shared/text-boundary.js'. room-share.js sits in the same shared/ directory, so it imports './text-boundary.js' — no new layer edge, no shared/ → host/ import.

Reproduction (live main ac81a4f1, probe test only, source untouched)

cleanPublishedName('a'.repeat(59) + '😀tail') returns a 60-unit string with isWellFormed() === false: the last code unit is a lone high surrogate (\ud83d) and the UTF-8 tail is 61 ef bf bd, i.e. …a + U+FFFD. Through the real call path (hooks.js:947-949) that ill-formed name lands in state.flybot.remote, so the sidebar, the @ menu and the roster all show U+FFFD. The same shape reproduces for tools.js:84 (60/40), reach.js:157 (60) and clip('a'.repeat(59) + '😀tail') in room-share.js (60).

Expected behaviour

Red first

Add the cases first, run them against ac81a4f1 with only the tests applied, and paste the failing output:

  1. core/test/flybot.test.js (the existing :811 group that already has 7 cases) — one 60-boundary emoji case through the real path: build the remote roster the way hooks.js:947-949 does and assert the stored name is well-formed, is a byte prefix of the input, and is <= 60. Illegal strength: do not assert "exactly 60"; assert the kept prefix and, for this input, that the straddling emoji is absent — otherwise a 60→40 mutation stays green.
  2. core/test/tools.test.js — the member line with displayName at its 60 boundary and ownerName at its 40 boundary.
  3. core/test/reach.test.js — the othersInRoom label at 60.
  4. core/test/room-share.test.js — clip at 60 and 120 (roster handle), asserting length <= max, well-formed, and that the parsed envelope keeps a byte prefix. room-share is the cross-owner case, so assert both properties in the same case.

Scope

  • core/src/host/hooks.js (:47), core/src/host/tools.js (:84), core/src/host/reach.js (:157), core/src/shared/room-share.js (:13-14) — the four bounds go through cut().
  • The four test files above; docs/subsystems/core-host.md (and the shared-envelope doc if one owns room-share); one changelog.d/<issue>-published-name-cut.fixed.md.
  • Regenerate patches/ds-bot/0001-core-fixes.patch and 0007-version-and-tooling.patch; check / canonical / generate no-op / TZ=UTC ARCH_DOCS_BASE=<base> npm run verify all green on the pushed tip.

Not in this change

hooks.js:353/:358 (verdict.recipient.slice(0, 120)), hooks.js:693/:697 (attachment name 200 / mime 120) and hooks.js:942 (avatarUpdatedAt 40) are the same shape but a different surface (queue verdicts and attachments); they get their own issue after this one. recall's 30-hit cap is a separate enhancement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions