From e47ef8cc282cf876a2b395f09f5acd5c845b2e8c Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:11:40 -0400 Subject: [PATCH] feat(mobile): the shell tells a page which optional capabilities it has (OTA phase D, C8.1) (#22141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mobile): publish page-route pairs the strict host schema accepts (OTA phase D, C8.1) `routeViewOf` handed the manifest's own route entries to the host as `pageRouteGrants`. The phone reads a manifest route loosely, so an entry arrives carrying whatever field the desktop that wrote it knew about, and `BridgePageRouteGrantsSchema` is `.strict()`: one unread key refuses the pairs, `createBridgeHost` refuses the route with them, and the page gets no `init` at all rather than losing one field. Fixed before any route carries an optional grant (ruling 37.4), so the manifest field the next commits add costs an installed shell nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * chore: drop the closure and bundle probe scripts from the tree Scratch measurements for C8.1 (which route closures reach the HTML preview, and what the preview render rig costs to bundle with a client provider). They belong outside the repository and were swept in by the previous commit's `git add -A`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): a manifest route may declare optional grants (OTA phase D, C8.1) Design B of design-ota-c8-1.md, ruling 37. `MobileWebBundleRouteSchema` grows `optionalGrants` under the required lane's own grammar, with the 16-name ceiling applied over the union of the two lists rather than to each. Serving a route still reads `grants` alone, so a capability a screen cannot work without stays required and takes the route native; a session's granted list is `[...grants, ...optionalGrants]` narrowed to what this shell implements, from one helper that both `grantsForRoute` and the `pageRouteGrants` publish read. The ruling's compatibility rationale is corrected in place. `z.looseObject` passes unknown members through rather than dropping them (measured, zod 4.4.3), so a shell older than the field still receives the key; what it lacks is a policy that reads one. What makes the lane safe against such a shell is therefore the previous commit's publish fix, not the reader. BRIDGE_PROTOCOL_VERSION stays 1. No new notify, verb or frame field. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): name the shell's cancelled-navigation behaviour as a grant (OTA phase D, C8.1) `externalNavigation` joins `MOBILE_WEB_SHELL_GRANTS` beside `screencastBinary` and `haptics`, declared in `cancelled-navigation-target.ts` because that is the module holding the rule which acts on it. A third token that is neither a verb nor a notify: the page posts nothing to make a cancelled top-frame navigation happen, so this list is the only thing that can tell a page whether a tap inside the sealed HTML-preview frame escapes at all. A constant and not a platform read (ruling 37.1): both engines dispatch the event, `ios/MobileWebShellView.swift:481` and Android's `MobileWebShellView.kt:382`, so an app build carries the behaviour on both or on neither. The policy census grows the half that was only pinned by the verb table: the implemented set is that table plus exactly three non-verb tokens, each read off the module that declares it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the bundle builder carries a route's optional grants (OTA phase D, C8.1) `resolveMobileWebPageRoutes` maps each declaration member by member, so a field the declaration grows reaches a phone only once the map names it: until now `optionalGrants` would have been dropped in silence and every route would have declared nothing optional. Omitted when the route declares none, because absent and empty are the same answer to a shell. The declaration suite grows the rule rather than a row: the map carries the lane through and writes no key without one, and the lane is held to the manifest's own grammar and to the ceiling over the union of the two lists. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the HTML preview hides its links on a shell that cannot open one (OTA phase D, C8.1) The session route declares `externalNavigation` on the optional lane, and the preview asks for it before it renders an artifact's links as links. Ruling 37.2's three readings are what "hide" means here, and removing `href` is what delivers all three at once: `a:any-link` stops matching, so the UA stylesheet stops underlining, the element leaves the tab order, and there is no dead anchor a tap does nothing on. The text the author wrote stays where it was, the artifact paints, and the Preview/Source toggle is untouched. Done with the browser's own parser rather than over the source text: an `href` inside a comment or a `