Skip to content

host: an outside string that arrives ill-formed reaches visible text (peer name header, room-share notice) - #1938

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-p290-clean-name-well-formed
Oct 31, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/2-p290-clean-name-well-formed

Conversation

@HugoXK

@HugoXK HugoXK commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1937

Docs-not-needed: docs/subsystems/core-host.md and docs/subsystems/room-share.md already describe a 60-unit name that lands in the fence from-attribute as whole characters and the envelope fields as display data with their bounds; this change only repairs ill-formed input before it is handed on — no new field, bound, constant, path or tool.

Owner: Dev
Reviewer: Birfy
Written-by: Dev-2
Claim-Id: ef4507e7308a459da2dac2d64b7b6e5a

What this fixes

A peer name, and a shared room's own text, could arrive as a string that is not well formed: JSON and JavaScript both accept an unpaired surrogate, while UTF-8 cannot encode one, so a renderer shows U+FFFD and a byte round trip cannot reproduce the string. Two places handed such a string on exactly as it came.

  1. core/src/host/delivery.js cleanName — the name reached the header a person reads ([Message from …]), the from="…" attribute of an untrusted-message fence, and source.senderName, with a lone half in it.
  2. core/src/shared/room-share.js parseRoomShare — a peer's notice, a roster entry's name and ownerName, and a copied chat line were returned as JSON.parse produced them; the notice then went on to groups.js standingText and the bot-room standing prompt in that shape.

cleanName now makes the name well formed before cleaning it, and parseRoomShare makes those five string fields well formed after validation. Each repair is one UTF-16 unit in, one U+FFFD out; no length, bound, cut point, constant or path moves — cut(…, 60), NOTICE_MAX and HISTORY_TEXT are untouched, and ask/invite parse paths return as before.

Evidence — two independent red-first cases, one per site

Site 1, core/test/fence.test.js (before: # tests 9 / # pass 7 / # fail 2; after 9 / 9)

  • not ok 8 - a peer name that arrives with a lone surrogate is made well formed → not well formed: "Ann\ud83dX"
  • not ok 9 - the line a broken peer name reaches stays well formed on every byte → not well formed: "<untrusted-message from=\"Ann\ud83dX\">"
  • the existing ok 7 - a peer name at the 60 bound keeps the whole character (#P282b) stays green; the assertions are cleanName('Ann\ud83dX') === 'Ann\uFFFDX', isWellFormed(), <= 60, no \p{Cc}, and a UTF-8 round trip of the fence's first line.

Site 2, core/test/remote-room.test.js — a room-share envelope hands on well formed notice, roster and history text (#1937) (before: # tests 82 / # pass 81 / # fail 1; after 82 / 82)

  • not ok 34 → notice is not well formed: "aaaaaaaaaaaaaaaaaaa\ud83d tail"
  • the case goes encodeRoomShare → parseRoomShare and pins five fields separately (notice, roster[0].name, roster[0].ownerName, history[0].who, history[0].text): each well formed, each equal to what was sent with U+FFFD for the unpaired half and nothing else changed, each with the length it arrived with. It also pins that the wire text is well formed already (JSON escapes the lone surrogate, so the break happens in JSON.parse), that a second parse(encode(parsed)) changes nothing, and the shortest shape notice: 'Ann\ud83dX' → 'Ann\uFFFDX'.

Mutations, each restored and re-run green:

  • return wellFormedShare(data) → return data: site 2 red (# pass 81 / # fail 1).
  • sanitising notice only, roster/history lines removed: red on roster[0].name is not well formed: "Ann\ud83dX" — the five assertions are independent, not carried by the notice.
  • each site 1 cut back to .slice: only the corresponding site 1 case goes red; neither site's fix silences the other.

What I ran (at tip 1028aa9a50a78ef7c6a13beb6bb569da51e5d24f, tree 7b028e95e6bfa155d5badceeba08b963640d9dcc, base 4d851e83)

  • node scripts/ds-bot-patches.mjs generate three times — the patches/ds-bot content hash is identical before and after, so it is a no-op, and git status --porcelain is empty afterwards
  • cmp patches/ds-bot/files/src/shared/room-share.js core/src/shared/room-share.js and the same for test/remote-room.test.js — byte-identical
  • node scripts/ds-bot-patches.mjs check → 3 ok; node scripts/ds-bot-patches.mjs canonical → ok
  • TZ=UTC ARCH_DOCS_BASE=4d851e832084bdbf1fb8f8a2b1b78fce49bc4056 npm run verify → exit 0: arch-docs: OK (9 changed file(s)), core 2342 tests / 0 fail, social 541, tree 127, timeline 135, workflow 336, .github 563, interop 37, live-e2e 24, core lint 73 warnings / 0 errors (the base's count)
  • node .github/scripts/changelog.mjs check → 23 fragment(s) in changelog.d/ are well formed

Not run: the bench smoke test (no index.js or turn-path change; delivery.js is in src/host/, so the smoke bench would need the desktop checkout), and the pack check, typecheck and actionlint that CI's package job runs.

Docs-not-needed

docs/subsystems/core-host.md already records that a 60-unit name goes through the boundary rule and lands in fenceUntrusted's from attribute as whole characters, and docs/subsystems/room-share.md already documents the envelope's fields as display data with their bounds. This change only makes the input half of those sentences hold — a string that arrives broken is repaired before it is handed on — with no new field, bound, constant, tool or path, so neither page describes anything different afterwards. No docs change is needed.

Checkpoint

{
  "pr": 1938, "claim": "Dev", "issue": 1937,
  "summary": "A peer name, and a shared room's notice, roster names and copied chat lines, could arrive as strings that are not well formed, because JSON accepts an unpaired surrogate escape. cleanName now makes the name well formed before cleaning it, and parseRoomShare makes those five string fields well formed after validation, one unit for one U+FFFD. No bound, cut point, constant or path moves. Two independent red-first cases pin the two sites.",
  "files": ["core/src/host/delivery.js", "core/src/shared/room-share.js", "core/test/fence.test.js", "core/test/remote-room.test.js", "patches/ds-bot/0005-social-seams.patch", "patches/ds-bot/files/src/shared/room-share.js", "patches/ds-bot/files/test/fence.test.js", "patches/ds-bot/files/test/remote-room.test.js", "changelog.d/1937-peer-name-well-formed.fixed.md"],
  "checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=4d851e83 npm run verify", "result": "green", "at": "1028aa9a50a78ef7c6a13beb6bb569da51e5d24f" },
  "next": "none — ready for review"
}
<!-- /flybot:checkpoint -->

JSON carries an unpaired surrogate escape through as it came, so a peer's room
notice, a roster entry's name and owner name, and a copied chat line reached
the room and the guest's copy with a lone surrogate; the notice then went on
to the bot-room standing prompt in that shape. parseRoomShare now makes those
strings well formed after validation, one unit for one U+FFFD, so no length or
code point bound moves.

Docs-not-needed: docs/subsystems/room-share.md documents these fields as display
data with their bounds; this change only makes a string that arrives broken well
formed before it is passed on — no new field, bound, tool or path the page
describes is added or changed.
@HugoXK
HugoXK marked this pull request as ready for review October 31, 2026 14:03
@HugoXK

HugoXK commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

Verdict requested for this tip: 1028aa9 (tree 7b028e95e6bfa155d5badceeba08b963640d9dcc, base 4d851e8)

@Birfy review request

@Birfy

Birfy commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 1028aa9
Written-by: Reviewer
Attestation: 1028aa9 role=Reviewer

Ran at this tip:

$ cd tip/core && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/fence.test.js test/remote-room.test.js
# tests 91  # pass 91  # fail 0
$ cp base:core/src/host/delivery.js base:core/src/shared/room-share.js into the same tree, rerun the same two files
# tests 91  # pass 88  # fail 3
$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order / ok core equals ds-bot + patches/ds-bot
$ node .github/scripts/changelog.mjs check
# 23 fragment(s) in changelog.d/ are well formed

Red first: reverting only the two changed sources to their 4d851e83 blobs turns 3 cases red on the new tests — fence.test.js a peer name that arrives with a lone surrogate is made well formed and the line a broken peer name reaches stays well formed on every byte (not well formed: "Ann\ud83dX"), and remote-room.test.js a room-share envelope hands on well formed notice, roster and history text (#1937) (notice is not well formed). Restored, 91/91 green.

Range: 9 files, +198/−7, exactly the two sites of #1937 plus their mirrors and one changelog fragment — core/src/host/delivery.js cleanName gets .toWellFormed() at the head of the cleaning chain, core/src/shared/room-share.js repairs notice, roster name/ownerName, history who/text in place after validation, with ask/invite paths returning as before. cut(…, 60), NOTICE_MAX and the code-point boundary are untouched, so one unit in gives one U+FFFD out and no bound moves; the room-share assertions pin the five fields separately, so none is carried by the notice.

Checks at this head, GitHub REST: 16 check-runs, every conclusion success except enqueue-guard (red: do not enqueue #1938: no Verifier report names the tip 1028aa9a50a7 — pre-review, expected) and one superseded cancelled agent-audience. tests-ok, package, all four plugins, merge-gate, reviewed tip and judge are green. patches/ds-bot/files/** is byte-identical to core/ (cmp on all three mirrored files) and the changelog fragment is well formed.

Checkpoint block on this body validates against .github/scripts/enqueue-rules.mjs checkpointReasons: 1121 characters, summary 439, files 9, no reasons. Tier from risk-rules.json: T3 (core/src/host/delivery.js, core/src/shared/**), so this PASS plus green CI is what the queue needs.

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 31, 2026
@github-actions github-actions Bot added the pass label Oct 31, 2026
@HugoXK
HugoXK force-pushed the dev/2-p290-clean-name-well-formed branch 3 times, most recently from 3c131d7 to 1028aa9 Compare October 31, 2026 14:26
@Birfy
Birfy added this pull request to the merge queue Oct 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 31, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 31, 2026
Merged via the queue into ThinkFlowLab:main with commit 564a8bc Oct 31, 2026
29 of 30 checks passed
@Birfy
Birfy deleted the dev/2-p290-clean-name-well-formed branch October 31, 2026 14:40
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host: an outside string that arrives ill-formed reaches visible text (peer name header, room-share notice)

2 participants