feat(chat): forward every message variant and add the Android host surface - #453
Merged
Conversation
decrypto21
force-pushed
the
feat/chat-message-surface
branch
from
August 19, 2026 13:45
9ff78c9 to
ec34ca4
Compare
TarikGul
reviewed
Aug 19, 2026
TarikGul
reviewed
Aug 19, 2026
decrypto21
force-pushed
the
feat/chat-message-surface
branch
from
August 19, 2026 20:28
ec34ca4 to
cf1fe40
Compare
TarikGul
approved these changes
Aug 19, 2026
johnthecat
added a commit
that referenced
this pull request
Aug 20, 2026
The main merge brought #453's chat work in against the renamed ProductExecutionKind. Git had no conflict to report: the enum definition came from this branch and the call sites came from #453, so App/Widget/Worker landed next to five uses of the Spa and Chat variants that no longer exist. Worker is the kind that serves Chat and App is the ordinary full-page product, so the mapping is a rename at each site. The host CLI keeps its own --execution spa|chat flag values, which name CLI roles rather than protocol kinds, so the batteries that pass them are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y65XQWkbMjfxG43gKvVV1b
26 tasks
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.
Summary
NativeChatCallbacks::post_text_messageandpost_custom_messagewith a singlepost_message(room_id, content), so a native host receives everyChatMessageContentvariant a product can post and rejects what it cannot render, instead of the core rejecting it firstChatPlatform, so every host flavour inherits the same limits, JS hosts included since feat(platform): serve Chat from a JS host as an optional capability #400httpsor an inline raster image, screened for smuggling characters before parsing, refused when it carries credentials, and measured against its budget after resolutionTrUAPIHostRuntime,TrUAPIProductExecutionandChatHostBridge, with buffered action publication, room-list republication, aFlowof typed UI for stored custom messages, and the session's X25519 chat identity keymake android-check, which compiles the Kotlin adapter against freshly generated bindingstruapi-host-clithrough an in-memory chat host, behind--execution-kind spa|chat, so a chat product is runnable headlessly at all--chat-hostbattery phase andmake e2e-chat-cli, nine cases against a real signing host with a real sessionThe wire protocol is unchanged. No wire ids, payload fields or enum variants move, and the only edits under
truapi/src/v01/andtruapi/src/api/chat.rsare doc comments.What the core screens, and what stays host-owned
The id a host returns from
post_messageis the correlation keyActionTrigger.message_idcarries back, which closes the loop from posting an action set, through a user triggering one, to the product reading the trigger.Ids and names are bounded, NFC-normalized and rejected for control and bidirectional characters. Message bodies are bounded at 16 KiB and screened, but neither trimmed nor normalized, so line breaks and tabs survive. Reaction emoji keep the tag characters that encode subdivision flags. A file name is screened as a path component as well as a label, so a host joining it onto a cache directory is never handed a separator or a parent reference, and a rejected one says so:
fileName must name a single file, not a path. Action and media lists cap at 32, custom payloads at 256 KiB, URLs at 2 KiB, icons at 64 KiB.is_body_unsafeandis_emoji_unsafeare deliberate carve-outs fromis_display_unsafe, and each test states what the carve-out must let through and why.Both URL fields resolve through one helper, so a budget is measured against the string a host receives rather than the one that arrived: the parser percent-encodes, and a non-ASCII path triples in the process. Credentials are refused, because
user:pass@survives resolution into whatever the host fetches and logs.Reachability is not screened, and the trait doc says so rather than leaving it implied:
https://127.0.0.1,https://[::1], a private range andhttps://169.254.169.254all pass, because which networks a host will fetch from depends on where that host runs, and a core that guessed would break a host serving its own media from localhost. A test pins those four as accepted so the behaviour cannot drift away from the sentence.ChatFile.size_bytesis a product assertion and is not verified against the resource it names. Contextual output escaping and storage limits remain host-owned.Live coverage
Chatrequests answerDeniedunless the connection opened as a Chat execution and a session is active, and the CLI host built every product asSpaand installed noChatPlatform. Nothing in the repo could exercise a chat product end to end, which is whyexplorer/diagnosis-reports/spa/signing-host-cli.mdcarries zeroChat/rows: the battery omits chat rather than failing it, the same shape that letChat::register_botsit unimplemented behind a report reading5 success, 0 failed.SigningHostRuntime::with_chat_platform(mirroring the pairing-host constructor), an in-memory chat host in the CLI, and a--chat-hostbattery phase close that: real product calls, over the wire, against a host holding a real session.The phase exists for a distinction unit tests cannot make. "The product got an error" and "the host was never handed the content" have the same product-visible symptom, and only the second is safe: a host that receives hostile content and refuses it is a host that has to be trusted to screen it again. The CLI host records every effect it accepted to
TRUAPI_CHAT_LOG, taggedroom,botormessage, and each case reads that record before and after its call, passing only when the product read an error and the host's record did not grow.Nine cases:
create_room; aTextbody with a newline and a tab that must reach the host byte for byte, compared againstChatMessageContent.encof what the product sent so a difference cannot hide behind a rendering; then ajavascript:file URL, a file name addressing a path, a URL carrying credentials, a URL and an icon that each arrive inside their budget and resolve past it, a body one byte past 16 KiB, and two action ids that NFC-normalize onto one key.The phase writes
explorer/diagnosis-reports/chat/signing-host-cli.md, committed next to the existingchat/ios.md, so the chat compatibility matrix gains a CLI column and a rerun shows its diff.spastays the default, so the existing battery phases and the SPA reports they own are untouched.Anyone can reproduce it:
make e2e-chat-cli, documented in both READMEs besidee2e-signing-cliande2e-pairing-cli.Breaking
NativeChatCallbacks.post_text_messageandpost_custom_messageare replaced bypost_message(roomId:content:). FFI symbols, vtable layout and checksums change, so the next@parity/ios-hostrelease is a breaking one.TrUAPIHostRuntime.openProductExecutiontakes(bridge:configuration:chat:)on both platforms. This is a source break for iOS callers, including hosts that do not serve Chat.TrUAPIProductExecutionProtocolrequiressessionChatIdentityKey().chat.postMessagerejects content it previously forwarded: text or file text over 16 KiB, URLs over 2 KiB or outside thehttps/inline-image allowlist, more than 32 actions or media items, custom payloads over 256 KiB, duplicate action ids, and control or bidirectional characters outside\n,\rand\t. A product sending any of these receivesMessageTooLargefor an over-budget body or payload, andUnknownnaming the field otherwise. There is no negotiation and no feature gate.chat.createRoomandchat.registerBoticons are screened for control and bidirectional characters and delivered as the URL parser resolved them, so a host comparing a stored icon against what a product sent seeshttps://x.invalidarrive ashttps://x.invalid/.Validation
cargo test --workspace --all-features: 978 passed, 15 ignoredcargo clippy --workspace --all-targets --all-features -- -D warningscargo +nightly fmt --checkcargo check --target wasm32-unknown-unknown -p truapi-server./scripts/codegen.shwith no tracked-file drift;make uniffireproduces the committed iOS bindings, andmake provider-swift-checkthe provider's@parity/truapi225 tests,@parity/truapi-host55 testsmake uniffi-kotlinreproduces the committed Kotlin bindings with no driftmake e2e-chat-cli: 9 cases against a live signing host, all passing--all-features --all-targetsTwenty three tests carry the claims. Sixteen sit with the predicates in
truapi-platform: each screened field is fed its own hostile value, a file name is fed both a bidi override that reverses its extension and a path that escapes its directory, a body is fed the text a person actually types, a reaction the emoji a person actually picks, a URL is fed a path that resolves past its budget on both fields and credentials on both, the four host-owned URLs are pinned as accepted, andthe_published_limits_are_the_enforced_limitspins the exported constants against what the code enforces so the two cannot drift. Four sit at the boundary: the native adapter forwards every variant, surfaces a host rejection as a typed error, round-trips a posted action set back to the product that posted it, and the runtime screens content before it reaches a host at all. An addedChatMessageContentvariant stopsvalidate_chat_message_contentcompiling rather than passing through unscreened.Three more cover the CLI chat host directly: a message into a room it never created is refused and leaves the transcript empty, a stored message is recorded with the payload bytes it arrived as, and a room created after a subscription starts reaches the list that subscription already holds.
make android-checkcompiles the Kotlin adapter, and was last run green on this surface. Gradle is not installed on the machine that produced the current revision, so it has not been re-run there;make uniffi-kotlinreproduces the committed bindings byte for byte and no Kotlin source has changed since, so the compile inputs are unchanged.Notes
Downstream iOS will not compile until it conforms. Making the protocol members required is deliberate: a silent protocol-extension default is exactly what let
register_botsit unimplemented for five weeks behind a diagnosis report that read "5 success, 0 failed".polkadot-app-ios-v2and the iOS host PR needsessionChatIdentityKey()and the newpost_messageshape.make e2e-chat-cliis a local gate, not a CI one. It needs a host process and a network-backed session, which is whye2e-signing-cliis not a CI job either, and why the repo's one E2E workflow job isif: false. Wiring live e2e into CI is real work with its own tradeoffs and is not attempted here.