@runtypelabs/persona 4.34.0 → 4.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/activity-ui.d.cts +3 -3
  2. package/dist/activity-ui.d.ts +3 -3
  3. package/dist/animations/glyph-cycle.d.cts +1 -1
  4. package/dist/animations/glyph-cycle.d.ts +1 -1
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/approval-ui.d.cts +3 -3
  8. package/dist/approval-ui.d.ts +3 -3
  9. package/dist/artifacts-ui.d.cts +3 -3
  10. package/dist/artifacts-ui.d.ts +3 -3
  11. package/dist/background-runtime.d.cts +1 -1
  12. package/dist/background-runtime.d.ts +1 -1
  13. package/dist/{chunk-CxNPSY-b.js → chunk-B1P8w9_X.js} +1 -1
  14. package/dist/{chunk-CV8EPIhT.js → chunk-B30Jdgg6.js} +1 -1
  15. package/dist/{chunk-6CUrH_jm.js → chunk-Bzo9GaAi.js} +1 -1
  16. package/dist/chunk-CNBMBx-Y.js +2 -0
  17. package/dist/chunk-Cd8mp8Sv.js +1 -0
  18. package/dist/chunk-CsfE8WRG.js +1 -0
  19. package/dist/{chunk-Co3VVwGe.js → chunk-D2pHRNEq.js} +0 -0
  20. package/dist/chunk-D3kZO8u-.js +1 -0
  21. package/dist/chunk-DAthf5H3.js +1 -0
  22. package/dist/{chunk-YMkMvAsa.js → chunk-DZ_aillY.js} +1 -1
  23. package/dist/{chunk-6C23QQdP.js → chunk-DgJV9zbK.js} +1 -1
  24. package/dist/chunk-Dv6KtIT1.js +1 -0
  25. package/dist/chunk-Dws8IrdR.js +1 -0
  26. package/dist/{chunk-DsWv3EGF.js → chunk-ThxxD8oK.js} +1 -1
  27. package/dist/client-dispatch.js +1 -1
  28. package/dist/client-history.js +1 -1
  29. package/dist/codegen.cjs +1 -1
  30. package/dist/codegen.js +1 -1
  31. package/dist/composer-actions-runtime.cjs +1 -1
  32. package/dist/composer-actions-runtime.d.cts +3 -3
  33. package/dist/composer-actions-runtime.d.ts +3 -3
  34. package/dist/composer-actions-runtime.js +1 -1
  35. package/dist/{composer-input-i_FiUp6J.d.cts → composer-input-BSDNPOqV.d.cts} +1 -1
  36. package/dist/{composer-input-i_FiUp6J.d.ts → composer-input-BSDNPOqV.d.ts} +1 -1
  37. package/dist/{composer-store-DhGNgG7Z.d.cts → composer-store-DC-gAjtN.d.cts} +1 -1
  38. package/dist/{composer-store-DhGNgG7Z.d.ts → composer-store-DC-gAjtN.d.ts} +1 -1
  39. package/dist/{context-mention-manager-DL-xwGfL.d.cts → context-mention-manager-CCpeoGL2.d.cts} +2 -2
  40. package/dist/{context-mention-manager-DL-xwGfL.d.ts → context-mention-manager-CCpeoGL2.d.ts} +2 -2
  41. package/dist/context-mentions-inline.d.cts +2 -2
  42. package/dist/context-mentions-inline.d.ts +2 -2
  43. package/dist/context-mentions.cjs +1 -1
  44. package/dist/context-mentions.d.cts +3 -3
  45. package/dist/context-mentions.d.ts +3 -3
  46. package/dist/context-mentions.js +1 -1
  47. package/dist/{defaults-Bga2Xrsp.d.cts → defaults-tj5RcN8r.d.cts} +1 -1
  48. package/dist/{defaults-Bga2Xrsp.d.ts → defaults-tj5RcN8r.d.ts} +1 -1
  49. package/dist/dictation-runtime.d.cts +2 -2
  50. package/dist/dictation-runtime.d.ts +2 -2
  51. package/dist/event-stream-view.d.cts +1 -1
  52. package/dist/event-stream-view.d.ts +1 -1
  53. package/dist/{fallback-speech-engine-kLdyPcmk.d.cts → fallback-speech-engine-gkhbjLiZ.d.cts} +1 -1
  54. package/dist/{fallback-speech-engine-kLdyPcmk.d.ts → fallback-speech-engine-gkhbjLiZ.d.ts} +1 -1
  55. package/dist/{feature-preferences-CmwZGKrf.d.cts → feature-preferences-CerDWGZ7.d.cts} +1 -1
  56. package/dist/{feature-preferences-CmwZGKrf.d.ts → feature-preferences-CerDWGZ7.d.ts} +1 -1
  57. package/dist/forms-ui.d.cts +2 -2
  58. package/dist/forms-ui.d.ts +2 -2
  59. package/dist/{history-confirm-dialog-i46-lPN_.d.cts → history-confirm-dialog-CrkSB3Ra.d.cts} +1 -1
  60. package/dist/{history-confirm-dialog-i46-lPN_.d.ts → history-confirm-dialog-CrkSB3Ra.d.ts} +1 -1
  61. package/dist/history-shell.d.cts +6 -6
  62. package/dist/history-shell.d.ts +6 -6
  63. package/dist/history-view.d.cts +1 -1
  64. package/dist/history-view.d.ts +1 -1
  65. package/dist/index.cjs +32 -32
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +8 -8
  68. package/dist/index.d.ts +8 -8
  69. package/dist/index.global.js +1 -1
  70. package/dist/index.global.js.map +1 -1
  71. package/dist/index.js +40 -40
  72. package/dist/index.js.map +1 -1
  73. package/dist/install.global.js +1 -1
  74. package/dist/launcher.global.js +1 -1
  75. package/dist/{loading-label-runtime-loader-Ccr3ETd3.d.cts → loading-label-runtime-loader-fWIRrhUF.d.cts} +1 -1
  76. package/dist/{loading-label-runtime-loader-Ccr3ETd3.d.ts → loading-label-runtime-loader-fWIRrhUF.d.ts} +1 -1
  77. package/dist/loading-label-runtime.d.cts +1 -1
  78. package/dist/loading-label-runtime.d.ts +1 -1
  79. package/dist/{message-bubble-C6AVaH7f.d.cts → message-bubble-Czc0lOHr.d.cts} +1 -1
  80. package/dist/{message-bubble-C6AVaH7f.d.ts → message-bubble-Czc0lOHr.d.ts} +1 -1
  81. package/dist/{plugin-kit-BVggqE3C.d.cts → plugin-kit-C30jE-cw.d.cts} +3 -1
  82. package/dist/{plugin-kit-BVggqE3C.d.ts → plugin-kit-C30jE-cw.d.ts} +3 -1
  83. package/dist/plugin-kit.cjs +1 -1
  84. package/dist/plugin-kit.d.cts +1 -1
  85. package/dist/plugin-kit.d.ts +1 -1
  86. package/dist/plugin-kit.js +1 -1
  87. package/dist/preferences-runtime.d.cts +1 -1
  88. package/dist/preferences-runtime.d.ts +1 -1
  89. package/dist/{preview-BMdRffo4.d.cts → preview-D-osV3FU.d.cts} +2 -2
  90. package/dist/{preview-BMdRffo4.d.ts → preview-D-osV3FU.d.ts} +2 -2
  91. package/dist/{registry-B16l8s1b.d.cts → registry-B6w91tx0.d.cts} +1 -1
  92. package/dist/{registry-B16l8s1b.d.ts → registry-B6w91tx0.d.ts} +1 -1
  93. package/dist/{session-CtZHe7fc.d.cts → session-BUQrs3Of.d.cts} +10 -9
  94. package/dist/{session-CtZHe7fc.d.ts → session-BUQrs3Of.d.ts} +10 -9
  95. package/dist/session-reconnect.d.cts +2 -2
  96. package/dist/session-reconnect.d.ts +2 -2
  97. package/dist/smart-dom-reader.d.cts +1 -1
  98. package/dist/smart-dom-reader.d.ts +1 -1
  99. package/dist/theme-editor-preview.cjs +43 -43
  100. package/dist/theme-editor-preview.d.cts +1 -1
  101. package/dist/theme-editor-preview.d.ts +1 -1
  102. package/dist/theme-editor-preview.js +3 -3
  103. package/dist/theme-editor.d.cts +2 -2
  104. package/dist/theme-editor.d.ts +2 -2
  105. package/dist/theme-reference.d.cts +2 -2
  106. package/dist/theme-reference.d.ts +2 -2
  107. package/dist/{tool-loading-animation-D2cyFvmM.d.cts → tool-loading-animation-B7XUf8TN.d.cts} +1 -1
  108. package/dist/{tool-loading-animation-D2cyFvmM.d.ts → tool-loading-animation-B7XUf8TN.d.ts} +1 -1
  109. package/dist/{turn-activity-ui-Do1Nd-Ow.d.cts → turn-activity-ui-CohuNMHT.d.cts} +2 -2
  110. package/dist/{turn-activity-ui-Do1Nd-Ow.d.ts → turn-activity-ui-CohuNMHT.d.ts} +2 -2
  111. package/dist/turn-activity-ui.d.cts +1 -1
  112. package/dist/turn-activity-ui.d.ts +1 -1
  113. package/dist/{types-BC7MRxO5.d.cts → types-9i0xcAMT.d.cts} +52 -0
  114. package/dist/{types-BC7MRxO5.d.ts → types-9i0xcAMT.d.ts} +52 -0
  115. package/dist/voice-client-tools.d.cts +1 -1
  116. package/dist/voice-client-tools.d.ts +1 -1
  117. package/dist/voice-runtime.cjs +1 -1
  118. package/dist/voice-runtime.d.cts +2 -2
  119. package/dist/voice-runtime.d.ts +2 -2
  120. package/dist/voice-runtime.js +1 -1
  121. package/dist/voice-worklet-player.d.cts +2 -2
  122. package/dist/voice-worklet-player.d.ts +2 -2
  123. package/dist/{webmcp-runtime-entry-B8g46vS0.d.ts → webmcp-runtime-entry-XNaXa-9h.d.cts} +9 -1
  124. package/dist/{webmcp-runtime-entry-B8g46vS0.d.cts → webmcp-runtime-entry-XNaXa-9h.d.ts} +9 -1
  125. package/dist/webmcp-runtime.cjs +1 -1
  126. package/dist/webmcp-runtime.d.cts +1 -1
  127. package/dist/webmcp-runtime.d.ts +1 -1
  128. package/dist/webmcp-runtime.js +1 -1
  129. package/package.json +2 -2
  130. package/src/client-dispatch.ts +13 -0
  131. package/src/client.ts +37 -2
  132. package/src/client.webmcp-require-confirm.test.ts +212 -0
  133. package/src/components/composer-model-picker.ts +0 -14
  134. package/src/components/composer-overflow-menu.ts +0 -15
  135. package/src/components/composer-tokens.ts +0 -20
  136. package/src/generated/runtype-openapi-contract.ts +13 -0
  137. package/src/plugin-kit.test.ts +101 -0
  138. package/src/plugin-kit.ts +29 -1
  139. package/src/session.ts +6 -2
  140. package/src/session.webmcp.test.ts +32 -0
  141. package/src/types.ts +35 -0
  142. package/src/ui.composer-models.test.ts +5 -7
  143. package/src/ui.composer-overflow.test.ts +14 -21
  144. package/src/utils/context-mention-controller.test.ts +56 -0
  145. package/src/webmcp-bridge.test.ts +102 -0
  146. package/src/webmcp-runtime-entry.ts +29 -1
  147. package/dist/chunk-BAkaTZpd.js +0 -1
  148. package/dist/chunk-BEDTp-dm.js +0 -1
  149. package/dist/chunk-C-rux8Q9.js +0 -1
  150. package/dist/chunk-CNhThrLX.js +0 -1
  151. package/dist/chunk-CzLEhoB_.js +0 -2
  152. package/dist/chunk-Dqa_GOcj.js +0 -1
  153. package/dist/chunk-NUNdcnAZ.js +0 -1
@@ -1,4 +1,4 @@
1
- import { Ca as WebMcpToolResult, Nn as AgentWidgetWebMcpConfig, rr as ClientToolDefinition, ta as VoiceClientToolOutcome, xa as WebMcpConfirmHandler } from "./types-BC7MRxO5.cjs";
1
+ import { Ca as WebMcpToolResult, Nn as AgentWidgetWebMcpConfig, rr as ClientToolDefinition, ta as VoiceClientToolOutcome, xa as WebMcpConfirmHandler } from "./types-9i0xcAMT.js";
2
2
  /** Server-applied wire prefix; strip when looking up registry entries. */
3
3
  declare const WEBMCP_TOOL_PREFIX = "webmcp:";
4
4
  /**
@@ -67,6 +67,14 @@ interface WebMcpBridgeDeps {
67
67
  getToolDisplayTitle: (toolName: string) => string | undefined;
68
68
  /** Core's `loadWebMcpPolyfillModule` (slot-aware polyfill resolution). */
69
69
  loadPolyfill: () => Promise<WebMcpPolyfillModule>;
70
+ /**
71
+ * Server-supplied `requireConfirmFor` patterns (the bound surface's policy
72
+ * from client-token init), read at call time because init can land after
73
+ * the bridge is built. Merged with `config.requireConfirmFor`. `null` means
74
+ * a policy is expected but not loaded yet: every call then needs
75
+ * confirmation (fail closed).
76
+ */
77
+ getRequireConfirmFor?: () => readonly string[] | null | undefined;
70
78
  }
71
79
  declare class WebMcpBridge {
72
80
  #private;
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=(e,t)=>({ok:!1,reason:e,message:t}),t=e=>e.startsWith(`webmcp:`)?e.slice(7):e,n={warn(e,...t){typeof console<`u`&&typeof console.warn==`function`&&console.warn(`[Persona/WebMCP] ${e}`,...t)}};var r=class{#e;#t;#n=!1;#r=null;#i=!1;#a;constructor(e,t){this.config=e,this.#a=t,this.#e=e.onConfirm??null,this.#t=3e4}setConfirmHandler(e){this.#e=e}isOperational(){return this.config.enabled!==!0||!this.#n?!1:this.#l()!==null}async snapshotForDispatch(){if(await this.#s(),this.config.enabled!==!0)return[];let e=this.#l();if(!e)return[];let t;try{t=(await e.getTools()).filter(h)}catch(e){return n.warn(`getTools() threw: shipping an empty WebMCP snapshot.`,e),[]}this.#a.recordToolDisplayTitles(t);let r=typeof location<`u`?location.origin:``;return t.filter(e=>this.#d(e.name)).map(e=>{let t=o(e.annotations);return{name:e.name,description:e.description,parametersSchema:s(e.inputSchema),origin:`webmcp`,...r?{pageOrigin:r}:{},...t?{annotations:t}:{},...t?.untrustedContentHint===!0?{untrustedContentHint:!0}:{}}})}async executeToolCall(e,t,n,r){let i=await this.#o(e,t,n,!1,r);return i.ok?c(i.raw):u(i.message)}async executeVoiceToolCall(e,t,n={}){let r=await this.#o(e,t,n.signal,n.autoConfirmReadOnly===!0);return r.ok?f(r.raw):{status:r.reason===`aborted`?`failed`:r.reason,error:{message:r.message}}}watchToolChanges(e){let t=!1,n=null;return this.#s().then(()=>{if(t||this.config.enabled!==!0)return;let r=this.#l();r&&typeof r.addEventListener==`function`&&(n=r,n.addEventListener(`toolchange`,e))}),()=>{t=!0,n?.removeEventListener(`toolchange`,e),n=null}}async#o(n,r,i,a,s){if(await this.#s(),this.config.enabled!==!0)return e(`failed`,`WebMCP is not enabled on this widget.`);let c=this.#l();if(!c){let t=typeof document<`u`&&!!document.modelContext;return e(`failed`,t?`WebMCP is not operational: document.modelContext is present but does not expose the strict getTools()/executeTool() surface (likely a different or older WebMCP polyfill).`:`WebMCP bridge is not operational on this page (document.modelContext not available).`)}let u=t(n),d;try{d=(await c.getTools()).filter(h)}catch(t){let n=t instanceof Error?t.message:String(t);return e(`failed`,`Failed to read WebMCP registry: ${n}`)}this.#a.recordToolDisplayTitles(d);let f=d.find(e=>e.name===u);if(!f)return e(`stale`,`WebMCP tool not registered on this page: ${u}`);if(!this.#d(u))return e(`stale`,`WebMCP tool not allowed by client allowlist: ${u}`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let p=this.#a.getToolDisplayTitle(u),m=o(f.annotations),g={toolName:u,...s?{toolCallId:s}:{},args:r,description:f.description,...m?{annotations:m}:{},...p?{title:p}:{},...i?{signal:i}:{},reason:`gate`};if(!(a&&m?.readOnlyHint===!0)&&!await this.#u(g))return e(`denied`,`User declined the tool call.`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let _=new AbortController,v=!1,y=setTimeout(()=>{v=!0,_.abort()},this.#t),b=()=>_.abort();i&&(i.aborted?_.abort():i.addEventListener(`abort`,b,{once:!0}));try{return{ok:!0,raw:await c.executeTool(f,l(r),{signal:_.signal})}}catch(t){if(v)return e(`timeout`,`WebMCP tool '${u}' timed out after ${this.#t}ms`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let n=t instanceof Error?t.message:String(t);return e(`failed`,n)}finally{clearTimeout(y),i&&i.removeEventListener(`abort`,b)}}#s(){return this.config.enabled===!0?(this.#r||=this.#c(),this.#r):Promise.resolve()}async#c(){try{if(this.#l()){this.#n=!0;return}let e=await this.#a.loadPolyfill();`initializeWebMCPPolyfill`in e?e.initializeWebMCPPolyfill():e.installWebMCP(),this.#n=!0}catch(e){n.warn(`Failed to load @mcp-b/webmcp-polyfill: WebMCP consumption disabled.`,e),this.#n=!1}}#l(){if(typeof document>`u`)return null;let e=document.modelContext;if(!e||typeof e!=`object`)return null;let t=e;return typeof t.getTools!=`function`||typeof t.executeTool!=`function`?(this.#i||(this.#i=!0,n.warn(`document.modelContext is present but does not expose getTools()/executeTool(): WebMCP consumption is disabled. Another (incompatible or older) WebMCP polyfill likely installed document.modelContext before Persona. Remove it, or use a polyfill implementing the strict standard surface (e.g. @mcp-b/webmcp-polyfill).`)),null):e}async#u(e){let t=this.#e??p;try{return await t(e)}catch(t){return n.warn(`Confirm handler threw for WebMCP tool '${e.toolName}'; declining.`,t),!1}}#d(e){let t=this.config.allowlist;return!t||t.length===0||t.some(t=>i(e,t))}};const i=(e,t)=>{if(t===`*`)return!0;let n=t.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);return RegExp(`^`+n.replace(/\*/g,`.*`)+`$`).test(e)},a={type:`object`,properties:{}},o=e=>{if(!e||typeof e!=`object`)return;let t={};for(let n of[`readOnlyHint`,`destructiveHint`,`consequentialHint`,`untrustedContentHint`,`idempotentHint`,`openWorldHint`,`debugging`]){let r=e[n];typeof r==`boolean`&&(t[n]=r)}return Object.keys(t).length>0?t:void 0},s=e=>{if(e===void 0||e===``)return{...a};if(typeof e==`object`)return e!==null&&!Array.isArray(e)?e:{...a};try{let t=JSON.parse(e);return typeof t==`object`&&t&&!Array.isArray(t)?t:{...a}}catch{return{...a}}},c=e=>{if(e==null)return{content:[{type:`text`,text:``}]};let t;try{t=JSON.parse(e)}catch{return{content:[{type:`text`,text:e}]}}return typeof t==`object`&&t&&Array.isArray(t.content)?t:{content:[{type:`text`,text:typeof t==`string`?t:g(t)}]}},l=e=>{let t;try{t=JSON.stringify(e)??`{}`}catch{t=`{}`}let n=JSON.parse(t);if(typeof n!=`object`||!n)throw TypeError(`WebMCP tool arguments must be an object or array.`);return Object.defineProperty(n,Symbol.toPrimitive,{value:()=>t}),n},u=e=>({isError:!0,content:[{type:`text`,text:e}]}),d=e=>Array.isArray(e)?e.map(e=>e&&typeof e==`object`&&typeof e.text==`string`?e.text:``).filter(Boolean).join(`
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=(e,t)=>({ok:!1,reason:e,message:t}),t=e=>e.startsWith(`webmcp:`)?e.slice(7):e,n={warn(e,...t){typeof console<`u`&&typeof console.warn==`function`&&console.warn(`[Persona/WebMCP] ${e}`,...t)}};var r=class{#e;#t;#n=!1;#r=null;#i=!1;#a;constructor(e,t){this.config=e,this.#a=t,this.#e=e.onConfirm??null,this.#t=3e4}setConfirmHandler(e){this.#e=e}isOperational(){return this.config.enabled!==!0||!this.#n?!1:this.#l()!==null}async snapshotForDispatch(){if(await this.#s(),this.config.enabled!==!0)return[];let e=this.#l();if(!e)return[];let t;try{t=(await e.getTools()).filter(h)}catch(e){return n.warn(`getTools() threw: shipping an empty WebMCP snapshot.`,e),[]}this.#a.recordToolDisplayTitles(t);let r=typeof location<`u`?location.origin:``;return t.filter(e=>this.#f(e.name)).map(e=>{let t=o(e.annotations);return{name:e.name,description:e.description,parametersSchema:s(e.inputSchema),origin:`webmcp`,...r?{pageOrigin:r}:{},...t?{annotations:t}:{},...t?.untrustedContentHint===!0?{untrustedContentHint:!0}:{}}})}async executeToolCall(e,t,n,r){let i=await this.#o(e,t,n,!1,r);return i.ok?c(i.raw):u(i.message)}async executeVoiceToolCall(e,t,n={}){let r=await this.#o(e,t,n.signal,n.autoConfirmReadOnly===!0);return r.ok?f(r.raw):{status:r.reason===`aborted`?`failed`:r.reason,error:{message:r.message}}}watchToolChanges(e){let t=!1,n=null;return this.#s().then(()=>{if(t||this.config.enabled!==!0)return;let r=this.#l();r&&typeof r.addEventListener==`function`&&(n=r,n.addEventListener(`toolchange`,e))}),()=>{t=!0,n?.removeEventListener(`toolchange`,e),n=null}}async#o(n,r,i,a,s){if(await this.#s(),this.config.enabled!==!0)return e(`failed`,`WebMCP is not enabled on this widget.`);let c=this.#l();if(!c){let t=typeof document<`u`&&!!document.modelContext;return e(`failed`,t?`WebMCP is not operational: document.modelContext is present but does not expose the strict getTools()/executeTool() surface (likely a different or older WebMCP polyfill).`:`WebMCP bridge is not operational on this page (document.modelContext not available).`)}let u=t(n),d;try{d=(await c.getTools()).filter(h)}catch(t){let n=t instanceof Error?t.message:String(t);return e(`failed`,`Failed to read WebMCP registry: ${n}`)}this.#a.recordToolDisplayTitles(d);let f=d.find(e=>e.name===u);if(!f)return e(`stale`,`WebMCP tool not registered on this page: ${u}`);if(!this.#f(u))return e(`stale`,`WebMCP tool not allowed by client allowlist: ${u}`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let p=this.#a.getToolDisplayTitle(u),m=o(f.annotations),g={toolName:u,...s?{toolCallId:s}:{},args:r,description:f.description,...m?{annotations:m}:{},...p?{title:p}:{},...i?{signal:i}:{},reason:`gate`},_=this.#d(u);if(_&&(g.confirmRequired=!0),(_||!a||m?.readOnlyHint!==!0)&&!await this.#u(g))return e(`denied`,`User declined the tool call.`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let v=new AbortController,y=!1,b=setTimeout(()=>{y=!0,v.abort()},this.#t),x=()=>v.abort();i&&(i.aborted?v.abort():i.addEventListener(`abort`,x,{once:!0}));try{return{ok:!0,raw:await c.executeTool(f,l(r),{signal:v.signal})}}catch(t){if(y)return e(`timeout`,`WebMCP tool '${u}' timed out after ${this.#t}ms`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let n=t instanceof Error?t.message:String(t);return e(`failed`,n)}finally{clearTimeout(b),i&&i.removeEventListener(`abort`,x)}}#s(){return this.config.enabled===!0?(this.#r||=this.#c(),this.#r):Promise.resolve()}async#c(){try{if(this.#l()){this.#n=!0;return}let e=await this.#a.loadPolyfill();`initializeWebMCPPolyfill`in e?e.initializeWebMCPPolyfill():e.installWebMCP(),this.#n=!0}catch(e){n.warn(`Failed to load @mcp-b/webmcp-polyfill: WebMCP consumption disabled.`,e),this.#n=!1}}#l(){if(typeof document>`u`)return null;let e=document.modelContext;if(!e||typeof e!=`object`)return null;let t=e;return typeof t.getTools!=`function`||typeof t.executeTool!=`function`?(this.#i||(this.#i=!0,n.warn(`document.modelContext is present but does not expose getTools()/executeTool(): WebMCP consumption is disabled. Another (incompatible or older) WebMCP polyfill likely installed document.modelContext before Persona. Remove it, or use a polyfill implementing the strict standard surface (e.g. @mcp-b/webmcp-polyfill).`)),null):e}async#u(e){let t=this.#e??p;try{return await t(e)}catch(t){return n.warn(`Confirm handler threw for WebMCP tool '${e.toolName}'; declining.`,t),!1}}#d(e){let t;try{t=this.#a.getRequireConfirmFor?.()}catch{t=null}if(t===null)return!0;let n=t=>Array.isArray(t)&&t.some(t=>typeof t==`string`&&i(e,t));return n(this.config.requireConfirmFor)||n(t)}#f(e){let t=this.config.allowlist;return!t||t.length===0||t.some(t=>i(e,t))}};const i=(e,t)=>{if(t===`*`)return!0;let n=t.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);return RegExp(`^`+n.replace(/\*/g,`.*`)+`$`).test(e)},a={type:`object`,properties:{}},o=e=>{if(!e||typeof e!=`object`)return;let t={};for(let n of[`readOnlyHint`,`destructiveHint`,`consequentialHint`,`untrustedContentHint`,`idempotentHint`,`openWorldHint`,`debugging`]){let r=e[n];typeof r==`boolean`&&(t[n]=r)}return Object.keys(t).length>0?t:void 0},s=e=>{if(e===void 0||e===``)return{...a};if(typeof e==`object`)return e!==null&&!Array.isArray(e)?e:{...a};try{let t=JSON.parse(e);return typeof t==`object`&&t&&!Array.isArray(t)?t:{...a}}catch{return{...a}}},c=e=>{if(e==null)return{content:[{type:`text`,text:``}]};let t;try{t=JSON.parse(e)}catch{return{content:[{type:`text`,text:e}]}}return typeof t==`object`&&t&&Array.isArray(t.content)?t:{content:[{type:`text`,text:typeof t==`string`?t:g(t)}]}},l=e=>{let t;try{t=JSON.stringify(e)??`{}`}catch{t=`{}`}let n=JSON.parse(t);if(typeof n!=`object`||!n)throw TypeError(`WebMCP tool arguments must be an object or array.`);return Object.defineProperty(n,Symbol.toPrimitive,{value:()=>t}),n},u=e=>({isError:!0,content:[{type:`text`,text:e}]}),d=e=>Array.isArray(e)?e.map(e=>e&&typeof e==`object`&&typeof e.text==`string`?e.text:``).filter(Boolean).join(`
2
2
  `):``,f=e=>{if(e==null)return{status:`completed`};let t;try{t=JSON.parse(e)}catch{return{status:`completed`,result:e}}return typeof t==`object`&&t&&t.isError===!0?{status:`failed`,error:{message:d(t.content)||`The page tool reported an error.`}}:t===void 0?{status:`completed`}:{status:`completed`,result:t}},p=async e=>{if(typeof window>`u`||typeof window.confirm!=`function`)return!1;let t=m(e.args),n=`Allow the AI to call ${e.toolName}`+(t?`\n\nArguments:\n${t}`:``)+(e.description?`\n\n${e.description}`:``);return window.confirm(n)},m=e=>{if(e==null)return``;try{let t=JSON.stringify(e,null,2);return t.length>500?t.slice(0,500)+`…`:t}catch{return String(e)}},h=e=>(e.window===void 0||typeof window<`u`&&e.window===window)&&(e.origin===void 0||typeof location<`u`&&e.origin===location.origin),g=e=>{if(e===void 0)return``;try{return JSON.stringify(e)}catch{return String(e)}};exports.WebMcpBridge=r;
@@ -1,3 +1,3 @@
1
- import { n as WebMcpBridgeDeps, t as WebMcpBridge } from "./webmcp-runtime-entry-B8g46vS0.cjs";
1
+ import { n as WebMcpBridgeDeps, t as WebMcpBridge } from "./webmcp-runtime-entry-XNaXa-9h.cjs";
2
2
  export { WebMcpBridge, type WebMcpBridgeDeps };
3
3
  export {}
@@ -1,3 +1,3 @@
1
- import { n as WebMcpBridgeDeps, t as WebMcpBridge } from "./webmcp-runtime-entry-B8g46vS0.js";
1
+ import { n as WebMcpBridgeDeps, t as WebMcpBridge } from "./webmcp-runtime-entry-XNaXa-9h.js";
2
2
  export { WebMcpBridge, type WebMcpBridgeDeps };
3
3
  export {}
@@ -1,2 +1,2 @@
1
- const e=(e,t)=>({ok:!1,reason:e,message:t}),t=e=>e.startsWith(`webmcp:`)?e.slice(7):e,n={warn(e,...t){typeof console<`u`&&typeof console.warn==`function`&&console.warn(`[Persona/WebMCP] ${e}`,...t)}};var r=class{#e;#t;#n=!1;#r=null;#i=!1;#a;constructor(e,t){this.config=e,this.#a=t,this.#e=e.onConfirm??null,this.#t=3e4}setConfirmHandler(e){this.#e=e}isOperational(){return this.config.enabled!==!0||!this.#n?!1:this.#l()!==null}async snapshotForDispatch(){if(await this.#s(),this.config.enabled!==!0)return[];let e=this.#l();if(!e)return[];let t;try{t=(await e.getTools()).filter(h)}catch(e){return n.warn(`getTools() threw: shipping an empty WebMCP snapshot.`,e),[]}this.#a.recordToolDisplayTitles(t);let r=typeof location<`u`?location.origin:``;return t.filter(e=>this.#d(e.name)).map(e=>{let t=o(e.annotations);return{name:e.name,description:e.description,parametersSchema:s(e.inputSchema),origin:`webmcp`,...r?{pageOrigin:r}:{},...t?{annotations:t}:{},...t?.untrustedContentHint===!0?{untrustedContentHint:!0}:{}}})}async executeToolCall(e,t,n,r){let i=await this.#o(e,t,n,!1,r);return i.ok?c(i.raw):u(i.message)}async executeVoiceToolCall(e,t,n={}){let r=await this.#o(e,t,n.signal,n.autoConfirmReadOnly===!0);return r.ok?f(r.raw):{status:r.reason===`aborted`?`failed`:r.reason,error:{message:r.message}}}watchToolChanges(e){let t=!1,n=null;return this.#s().then(()=>{if(t||this.config.enabled!==!0)return;let r=this.#l();r&&typeof r.addEventListener==`function`&&(n=r,n.addEventListener(`toolchange`,e))}),()=>{t=!0,n?.removeEventListener(`toolchange`,e),n=null}}async#o(n,r,i,a,s){if(await this.#s(),this.config.enabled!==!0)return e(`failed`,`WebMCP is not enabled on this widget.`);let c=this.#l();if(!c){let t=typeof document<`u`&&!!document.modelContext;return e(`failed`,t?`WebMCP is not operational: document.modelContext is present but does not expose the strict getTools()/executeTool() surface (likely a different or older WebMCP polyfill).`:`WebMCP bridge is not operational on this page (document.modelContext not available).`)}let u=t(n),d;try{d=(await c.getTools()).filter(h)}catch(t){let n=t instanceof Error?t.message:String(t);return e(`failed`,`Failed to read WebMCP registry: ${n}`)}this.#a.recordToolDisplayTitles(d);let f=d.find(e=>e.name===u);if(!f)return e(`stale`,`WebMCP tool not registered on this page: ${u}`);if(!this.#d(u))return e(`stale`,`WebMCP tool not allowed by client allowlist: ${u}`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let p=this.#a.getToolDisplayTitle(u),m=o(f.annotations),g={toolName:u,...s?{toolCallId:s}:{},args:r,description:f.description,...m?{annotations:m}:{},...p?{title:p}:{},...i?{signal:i}:{},reason:`gate`};if(!(a&&m?.readOnlyHint===!0)&&!await this.#u(g))return e(`denied`,`User declined the tool call.`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let _=new AbortController,v=!1,y=setTimeout(()=>{v=!0,_.abort()},this.#t),b=()=>_.abort();i&&(i.aborted?_.abort():i.addEventListener(`abort`,b,{once:!0}));try{return{ok:!0,raw:await c.executeTool(f,l(r),{signal:_.signal})}}catch(t){if(v)return e(`timeout`,`WebMCP tool '${u}' timed out after ${this.#t}ms`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let n=t instanceof Error?t.message:String(t);return e(`failed`,n)}finally{clearTimeout(y),i&&i.removeEventListener(`abort`,b)}}#s(){return this.config.enabled===!0?(this.#r||=this.#c(),this.#r):Promise.resolve()}async#c(){try{if(this.#l()){this.#n=!0;return}let e=await this.#a.loadPolyfill();`initializeWebMCPPolyfill`in e?e.initializeWebMCPPolyfill():e.installWebMCP(),this.#n=!0}catch(e){n.warn(`Failed to load @mcp-b/webmcp-polyfill: WebMCP consumption disabled.`,e),this.#n=!1}}#l(){if(typeof document>`u`)return null;let e=document.modelContext;if(!e||typeof e!=`object`)return null;let t=e;return typeof t.getTools!=`function`||typeof t.executeTool!=`function`?(this.#i||(this.#i=!0,n.warn(`document.modelContext is present but does not expose getTools()/executeTool(): WebMCP consumption is disabled. Another (incompatible or older) WebMCP polyfill likely installed document.modelContext before Persona. Remove it, or use a polyfill implementing the strict standard surface (e.g. @mcp-b/webmcp-polyfill).`)),null):e}async#u(e){let t=this.#e??p;try{return await t(e)}catch(t){return n.warn(`Confirm handler threw for WebMCP tool '${e.toolName}'; declining.`,t),!1}}#d(e){let t=this.config.allowlist;return!t||t.length===0||t.some(t=>i(e,t))}};const i=(e,t)=>{if(t===`*`)return!0;let n=t.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);return RegExp(`^`+n.replace(/\*/g,`.*`)+`$`).test(e)},a={type:`object`,properties:{}},o=e=>{if(!e||typeof e!=`object`)return;let t={};for(let n of[`readOnlyHint`,`destructiveHint`,`consequentialHint`,`untrustedContentHint`,`idempotentHint`,`openWorldHint`,`debugging`]){let r=e[n];typeof r==`boolean`&&(t[n]=r)}return Object.keys(t).length>0?t:void 0},s=e=>{if(e===void 0||e===``)return{...a};if(typeof e==`object`)return e!==null&&!Array.isArray(e)?e:{...a};try{let t=JSON.parse(e);return typeof t==`object`&&t&&!Array.isArray(t)?t:{...a}}catch{return{...a}}},c=e=>{if(e==null)return{content:[{type:`text`,text:``}]};let t;try{t=JSON.parse(e)}catch{return{content:[{type:`text`,text:e}]}}return typeof t==`object`&&t&&Array.isArray(t.content)?t:{content:[{type:`text`,text:typeof t==`string`?t:g(t)}]}},l=e=>{let t;try{t=JSON.stringify(e)??`{}`}catch{t=`{}`}let n=JSON.parse(t);if(typeof n!=`object`||!n)throw TypeError(`WebMCP tool arguments must be an object or array.`);return Object.defineProperty(n,Symbol.toPrimitive,{value:()=>t}),n},u=e=>({isError:!0,content:[{type:`text`,text:e}]}),d=e=>Array.isArray(e)?e.map(e=>e&&typeof e==`object`&&typeof e.text==`string`?e.text:``).filter(Boolean).join(`
1
+ const e=(e,t)=>({ok:!1,reason:e,message:t}),t=e=>e.startsWith(`webmcp:`)?e.slice(7):e,n={warn(e,...t){typeof console<`u`&&typeof console.warn==`function`&&console.warn(`[Persona/WebMCP] ${e}`,...t)}};var r=class{#e;#t;#n=!1;#r=null;#i=!1;#a;constructor(e,t){this.config=e,this.#a=t,this.#e=e.onConfirm??null,this.#t=3e4}setConfirmHandler(e){this.#e=e}isOperational(){return this.config.enabled!==!0||!this.#n?!1:this.#l()!==null}async snapshotForDispatch(){if(await this.#s(),this.config.enabled!==!0)return[];let e=this.#l();if(!e)return[];let t;try{t=(await e.getTools()).filter(h)}catch(e){return n.warn(`getTools() threw: shipping an empty WebMCP snapshot.`,e),[]}this.#a.recordToolDisplayTitles(t);let r=typeof location<`u`?location.origin:``;return t.filter(e=>this.#f(e.name)).map(e=>{let t=o(e.annotations);return{name:e.name,description:e.description,parametersSchema:s(e.inputSchema),origin:`webmcp`,...r?{pageOrigin:r}:{},...t?{annotations:t}:{},...t?.untrustedContentHint===!0?{untrustedContentHint:!0}:{}}})}async executeToolCall(e,t,n,r){let i=await this.#o(e,t,n,!1,r);return i.ok?c(i.raw):u(i.message)}async executeVoiceToolCall(e,t,n={}){let r=await this.#o(e,t,n.signal,n.autoConfirmReadOnly===!0);return r.ok?f(r.raw):{status:r.reason===`aborted`?`failed`:r.reason,error:{message:r.message}}}watchToolChanges(e){let t=!1,n=null;return this.#s().then(()=>{if(t||this.config.enabled!==!0)return;let r=this.#l();r&&typeof r.addEventListener==`function`&&(n=r,n.addEventListener(`toolchange`,e))}),()=>{t=!0,n?.removeEventListener(`toolchange`,e),n=null}}async#o(n,r,i,a,s){if(await this.#s(),this.config.enabled!==!0)return e(`failed`,`WebMCP is not enabled on this widget.`);let c=this.#l();if(!c){let t=typeof document<`u`&&!!document.modelContext;return e(`failed`,t?`WebMCP is not operational: document.modelContext is present but does not expose the strict getTools()/executeTool() surface (likely a different or older WebMCP polyfill).`:`WebMCP bridge is not operational on this page (document.modelContext not available).`)}let u=t(n),d;try{d=(await c.getTools()).filter(h)}catch(t){let n=t instanceof Error?t.message:String(t);return e(`failed`,`Failed to read WebMCP registry: ${n}`)}this.#a.recordToolDisplayTitles(d);let f=d.find(e=>e.name===u);if(!f)return e(`stale`,`WebMCP tool not registered on this page: ${u}`);if(!this.#f(u))return e(`stale`,`WebMCP tool not allowed by client allowlist: ${u}`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let p=this.#a.getToolDisplayTitle(u),m=o(f.annotations),g={toolName:u,...s?{toolCallId:s}:{},args:r,description:f.description,...m?{annotations:m}:{},...p?{title:p}:{},...i?{signal:i}:{},reason:`gate`},_=this.#d(u);if(_&&(g.confirmRequired=!0),(_||!a||m?.readOnlyHint!==!0)&&!await this.#u(g))return e(`denied`,`User declined the tool call.`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let v=new AbortController,y=!1,b=setTimeout(()=>{y=!0,v.abort()},this.#t),x=()=>v.abort();i&&(i.aborted?v.abort():i.addEventListener(`abort`,x,{once:!0}));try{return{ok:!0,raw:await c.executeTool(f,l(r),{signal:v.signal})}}catch(t){if(y)return e(`timeout`,`WebMCP tool '${u}' timed out after ${this.#t}ms`);if(i?.aborted)return e(`aborted`,`Aborted by cancel()`);let n=t instanceof Error?t.message:String(t);return e(`failed`,n)}finally{clearTimeout(b),i&&i.removeEventListener(`abort`,x)}}#s(){return this.config.enabled===!0?(this.#r||=this.#c(),this.#r):Promise.resolve()}async#c(){try{if(this.#l()){this.#n=!0;return}let e=await this.#a.loadPolyfill();`initializeWebMCPPolyfill`in e?e.initializeWebMCPPolyfill():e.installWebMCP(),this.#n=!0}catch(e){n.warn(`Failed to load @mcp-b/webmcp-polyfill: WebMCP consumption disabled.`,e),this.#n=!1}}#l(){if(typeof document>`u`)return null;let e=document.modelContext;if(!e||typeof e!=`object`)return null;let t=e;return typeof t.getTools!=`function`||typeof t.executeTool!=`function`?(this.#i||(this.#i=!0,n.warn(`document.modelContext is present but does not expose getTools()/executeTool(): WebMCP consumption is disabled. Another (incompatible or older) WebMCP polyfill likely installed document.modelContext before Persona. Remove it, or use a polyfill implementing the strict standard surface (e.g. @mcp-b/webmcp-polyfill).`)),null):e}async#u(e){let t=this.#e??p;try{return await t(e)}catch(t){return n.warn(`Confirm handler threw for WebMCP tool '${e.toolName}'; declining.`,t),!1}}#d(e){let t;try{t=this.#a.getRequireConfirmFor?.()}catch{t=null}if(t===null)return!0;let n=t=>Array.isArray(t)&&t.some(t=>typeof t==`string`&&i(e,t));return n(this.config.requireConfirmFor)||n(t)}#f(e){let t=this.config.allowlist;return!t||t.length===0||t.some(t=>i(e,t))}};const i=(e,t)=>{if(t===`*`)return!0;let n=t.replace(/[.+?^${}()|[\]\\]/g,`\\$&`);return RegExp(`^`+n.replace(/\*/g,`.*`)+`$`).test(e)},a={type:`object`,properties:{}},o=e=>{if(!e||typeof e!=`object`)return;let t={};for(let n of[`readOnlyHint`,`destructiveHint`,`consequentialHint`,`untrustedContentHint`,`idempotentHint`,`openWorldHint`,`debugging`]){let r=e[n];typeof r==`boolean`&&(t[n]=r)}return Object.keys(t).length>0?t:void 0},s=e=>{if(e===void 0||e===``)return{...a};if(typeof e==`object`)return e!==null&&!Array.isArray(e)?e:{...a};try{let t=JSON.parse(e);return typeof t==`object`&&t&&!Array.isArray(t)?t:{...a}}catch{return{...a}}},c=e=>{if(e==null)return{content:[{type:`text`,text:``}]};let t;try{t=JSON.parse(e)}catch{return{content:[{type:`text`,text:e}]}}return typeof t==`object`&&t&&Array.isArray(t.content)?t:{content:[{type:`text`,text:typeof t==`string`?t:g(t)}]}},l=e=>{let t;try{t=JSON.stringify(e)??`{}`}catch{t=`{}`}let n=JSON.parse(t);if(typeof n!=`object`||!n)throw TypeError(`WebMCP tool arguments must be an object or array.`);return Object.defineProperty(n,Symbol.toPrimitive,{value:()=>t}),n},u=e=>({isError:!0,content:[{type:`text`,text:e}]}),d=e=>Array.isArray(e)?e.map(e=>e&&typeof e==`object`&&typeof e.text==`string`?e.text:``).filter(Boolean).join(`
2
2
  `):``,f=e=>{if(e==null)return{status:`completed`};let t;try{t=JSON.parse(e)}catch{return{status:`completed`,result:e}}return typeof t==`object`&&t&&t.isError===!0?{status:`failed`,error:{message:d(t.content)||`The page tool reported an error.`}}:t===void 0?{status:`completed`}:{status:`completed`,result:t}},p=async e=>{if(typeof window>`u`||typeof window.confirm!=`function`)return!1;let t=m(e.args),n=`Allow the AI to call ${e.toolName}`+(t?`\n\nArguments:\n${t}`:``)+(e.description?`\n\n${e.description}`:``);return window.confirm(n)},m=e=>{if(e==null)return``;try{let t=JSON.stringify(e,null,2);return t.length>500?t.slice(0,500)+`…`:t}catch{return String(e)}},h=e=>(e.window===void 0||typeof window<`u`&&e.window===window)&&(e.origin===void 0||typeof location<`u`&&e.origin===location.origin),g=e=>{if(e===void 0)return``;try{return JSON.stringify(e)}catch{return String(e)}};export{r as WebMcpBridge};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "4.34.0",
3
+ "version": "4.35.1",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -235,7 +235,7 @@
235
235
  "check:size": "node scripts/check-size.mjs",
236
236
  "check:dist-vite8": "node scripts/check-dist-no-in-for-init.mjs",
237
237
  "build:styles": "node scripts/build-styles.mjs",
238
- "lint": "oxlint",
238
+ "lint": "node ../../scripts/lint.mjs",
239
239
  "typecheck": "pnpm run check:runtype-types && node ../../scripts/typecheck.mjs --noEmit",
240
240
  "test": "vitest",
241
241
  "test:ui": "vitest --ui",
@@ -394,6 +394,8 @@ export class ClientDispatch {
394
394
  welcomeMessage: data.config.welcomeMessage,
395
395
  placeholder: data.config.placeholder,
396
396
  theme: data.config.theme as ClientSession["config"]["theme"],
397
+ // Kept only as a non-empty array; the bridge ignores non-string entries.
398
+ ...readWebMcpInitConfig(data.config),
397
399
  },
398
400
  };
399
401
  }
@@ -1087,3 +1089,14 @@ export class ClientDispatch {
1087
1089
  }
1088
1090
 
1089
1091
  }
1092
+
1093
+ /** `config.webmcp` from `/v1/client/init`, kept only with a non-empty pattern array. */
1094
+ const readWebMcpInitConfig = (
1095
+ config: unknown
1096
+ ): Pick<ClientSession["config"], "webmcp"> => {
1097
+ const list = (config as { webmcp?: { requireConfirmFor?: unknown } }).webmcp
1098
+ ?.requireConfirmFor;
1099
+ return Array.isArray(list) && list.length
1100
+ ? { webmcp: { requireConfirmFor: list as string[] } }
1101
+ : {};
1102
+ };
package/src/client.ts CHANGED
@@ -181,7 +181,18 @@ export class AgentWidgetClient {
181
181
  * matches is superseded, and every SSE frame it still receives is dropped
182
182
  */
183
183
  #currentClientTurnId: string | null = null;
184
- private clientSession: ClientSession | null = null;
184
+ #clientSession: ClientSession | null = null;
185
+ /** Last surface `requireConfirmFor` installed with a session; `null` until one loads. */
186
+ #knownRequireConfirmFor: readonly string[] | null = null;
187
+ // Every install path assigns here, so the surface policy is recorded the
188
+ // moment a session lands, not when a tool call next reads it.
189
+ private get clientSession(): ClientSession | null {
190
+ return this.#clientSession;
191
+ }
192
+ private set clientSession(session: ClientSession | null) {
193
+ this.#clientSession = session;
194
+ if (session) this.#knownRequireConfirmFor = session.config?.webmcp?.requireConfirmFor ?? [];
195
+ }
185
196
  #sessionInitPromise: Promise<ClientSession> | null = null;
186
197
  /**
187
198
  * Early-init latch for `warmSession()`. `false` = armed. Otherwise it holds
@@ -275,6 +286,7 @@ export class AgentWidgetClient {
275
286
  recordToolDisplayTitles: recordWebMcpToolDisplayTitles,
276
287
  getToolDisplayTitle: getWebMcpToolDisplayTitle,
277
288
  loadPolyfill: loadWebMcpPolyfillModule,
289
+ getRequireConfirmFor: () => this.#surfaceRequireConfirmFor(),
278
290
  });
279
291
  if (this.#pendingWebMcpConfirmHandler !== undefined) {
280
292
  bridge.setConfirmHandler(this.#pendingWebMcpConfirmHandler);
@@ -408,12 +420,35 @@ export class AgentWidgetClient {
408
420
  if (!b) {
409
421
  return { status: "failed", error: { message: "WebMCP runtime failed to load." } };
410
422
  }
411
- return b.executeVoiceToolCall(name, args, options);
423
+ return b.executeVoiceToolCall(name, args, await this.#voiceToolOptions(options));
412
424
  },
413
425
  };
414
426
  return surface;
415
427
  }
416
428
 
429
+ /**
430
+ * The bound surface's `requireConfirmFor` for the WebMCP bridge. Sticky: the
431
+ * policy of the last installed session survives `clearClientSession()` (same
432
+ * token, same surface), so a session cleared mid-call cannot drop it. `null`
433
+ * means client-token mode with no policy loaded yet; the bridge then
434
+ * confirms every call. `undefined` outside client-token mode.
435
+ */
436
+ #surfaceRequireConfirmFor(): readonly string[] | null | undefined {
437
+ return this.isClientTokenMode() ? this.#knownRequireConfirmFor : undefined;
438
+ }
439
+
440
+ /**
441
+ * A voice call can start before any typed turn has run client-token init.
442
+ * Try to load the surface policy first so read-only tools keep their fast
443
+ * tier; on failure the policy stays unknown and the bridge gates the call.
444
+ */
445
+ async #voiceToolOptions<T extends { autoConfirmReadOnly: boolean }>(options: T): Promise<T> {
446
+ if (options.autoConfirmReadOnly && this.isClientTokenMode() && !this.clientSession) {
447
+ await this.initSession().catch(() => undefined);
448
+ }
449
+ return options;
450
+ }
451
+
417
452
  public executeWebMcpToolCall(
418
453
  wireToolName: string,
419
454
  args: unknown,
@@ -0,0 +1,212 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { AgentWidgetClient } from "./client";
6
+ import type { AgentWidgetConfig, ClientInitResponse, WebMcpConfirmHandler } from "./types";
7
+
8
+ const API_URL = "https://api.runtype.com";
9
+
10
+ const initBody = (config: Record<string, unknown> = {}): ClientInitResponse =>
11
+ ({
12
+ sessionId: "sess_1",
13
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
14
+ flow: { id: "flow_1", name: "Flow", description: null },
15
+ config: { welcomeMessage: null, placeholder: "Ask...", theme: null, ...config },
16
+ }) as ClientInitResponse;
17
+
18
+ const installInit = (body: ClientInitResponse) => {
19
+ global.fetch = vi.fn(async () => ({
20
+ ok: true,
21
+ status: 200,
22
+ json: async () => body,
23
+ })) as unknown as typeof fetch;
24
+ };
25
+
26
+ const makeClient = (webmcp?: AgentWidgetConfig["webmcp"]) =>
27
+ new AgentWidgetClient({
28
+ apiUrl: API_URL,
29
+ clientToken: "ct_live_x",
30
+ ...(webmcp ? { webmcp } : {}),
31
+ });
32
+
33
+ afterEach(() => {
34
+ vi.restoreAllMocks();
35
+ delete (document as Document & { modelContext?: unknown }).modelContext;
36
+ });
37
+
38
+ describe("client-token init: config.webmcp.requireConfirmFor", () => {
39
+ it("keeps the surface's patterns on the session", async () => {
40
+ installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
41
+ const session = await makeClient().initSession();
42
+ expect(session.config.webmcp).toEqual({ requireConfirmFor: ["checkout_*"] });
43
+ });
44
+
45
+ it.each([
46
+ ["absent", {}],
47
+ ["empty", { webmcp: { requireConfirmFor: [] } }],
48
+ ["malformed", { webmcp: { requireConfirmFor: "checkout_*" } }],
49
+ ])("omits config.webmcp when the list is %s", async (_label, config) => {
50
+ installInit(initBody(config));
51
+ const session = await makeClient().initSession();
52
+ expect(session.config).not.toHaveProperty("webmcp");
53
+ });
54
+ });
55
+
56
+ describe("client-token init feeds the WebMCP confirm gate", () => {
57
+ it("flags a matched page tool for the host's onConfirm after init", async () => {
58
+ const execute = vi.fn(async () => JSON.stringify("ran"));
59
+ (document as Document & { modelContext?: unknown }).modelContext = {
60
+ getTools: async () => [
61
+ { name: "checkout_submit", description: "Place the order", title: "", window, origin: location.origin },
62
+ { name: "search", description: "Search", title: "", window, origin: location.origin },
63
+ ],
64
+ executeTool: execute,
65
+ };
66
+ installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
67
+ const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
68
+ const client = makeClient({ enabled: true, onConfirm });
69
+ await client.initSession();
70
+
71
+ await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
72
+ await client.executeWebMcpToolCall("webmcp:search", {});
73
+
74
+ expect(onConfirm).toHaveBeenCalledTimes(2);
75
+ expect(onConfirm.mock.calls[0]?.[0]).toMatchObject({
76
+ toolName: "checkout_submit",
77
+ confirmRequired: true,
78
+ });
79
+ expect(onConfirm.mock.calls[1]?.[0]).not.toHaveProperty("confirmRequired");
80
+ expect(execute).toHaveBeenCalledTimes(2);
81
+ });
82
+ });
83
+
84
+ describe("voice before any client-token init", () => {
85
+ const installReadOnlyTools = () => {
86
+ (document as Document & { modelContext?: unknown }).modelContext = {
87
+ getTools: async () =>
88
+ ["checkout_preview", "next_slide"].map((name) => ({
89
+ name,
90
+ description: name,
91
+ title: "",
92
+ window,
93
+ origin: location.origin,
94
+ annotations: { readOnlyHint: true },
95
+ })),
96
+ executeTool: vi.fn(async () => JSON.stringify("ran")),
97
+ };
98
+ };
99
+ const voiceExecute = (client: AgentWidgetClient, name: string) =>
100
+ client
101
+ .getVoiceClientTools()!
102
+ .execute(name, {}, { signal: new AbortController().signal, autoConfirmReadOnly: true });
103
+
104
+ it("loads the surface policy so a matched read-only tool still confirms", async () => {
105
+ installReadOnlyTools();
106
+ installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
107
+ const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => false);
108
+ const client = makeClient({ enabled: true, onConfirm });
109
+
110
+ const matched = await voiceExecute(client, "webmcp:checkout_preview");
111
+ const unmatched = await voiceExecute(client, "webmcp:next_slide");
112
+
113
+ expect(global.fetch).toHaveBeenCalledTimes(1);
114
+ expect(onConfirm).toHaveBeenCalledTimes(1);
115
+ expect(onConfirm.mock.calls[0]?.[0]).toMatchObject({
116
+ toolName: "checkout_preview",
117
+ confirmRequired: true,
118
+ });
119
+ expect(matched.status).toBe("denied");
120
+ expect(unmatched.status).toBe("completed");
121
+ });
122
+
123
+ it("keeps every voice call gated when init fails", async () => {
124
+ installReadOnlyTools();
125
+ global.fetch = vi.fn(async () => {
126
+ throw new Error("offline");
127
+ }) as unknown as typeof fetch;
128
+ const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => false);
129
+ const client = makeClient({ enabled: true, onConfirm });
130
+
131
+ const result = await voiceExecute(client, "webmcp:next_slide");
132
+
133
+ expect(onConfirm).toHaveBeenCalledTimes(1);
134
+ expect(result.status).toBe("denied");
135
+ });
136
+ });
137
+
138
+ describe("surface policy lifetime", () => {
139
+ const installTools = () => {
140
+ (document as Document & { modelContext?: unknown }).modelContext = {
141
+ getTools: async () =>
142
+ ["checkout_submit", "search"].map((name) => ({
143
+ name,
144
+ description: name,
145
+ title: "",
146
+ window,
147
+ origin: location.origin,
148
+ })),
149
+ executeTool: vi.fn(async () => JSON.stringify("ran")),
150
+ };
151
+ };
152
+
153
+ it("keeps the last loaded policy after clearClientSession()", async () => {
154
+ installTools();
155
+ installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
156
+ const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
157
+ const client = makeClient({ enabled: true, onConfirm });
158
+ await client.initSession();
159
+ // Read the policy once while the session is live, then drop the session.
160
+ await client.executeWebMcpToolCall("webmcp:search", {});
161
+ client.clearClientSession();
162
+
163
+ await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
164
+ await client.executeWebMcpToolCall("webmcp:search", {});
165
+
166
+ expect(onConfirm.mock.calls.map((c) => c[0]?.confirmRequired)).toEqual([
167
+ undefined,
168
+ true,
169
+ undefined,
170
+ ]);
171
+ });
172
+
173
+ it("records a policy when a session installs, not when a call reads it", async () => {
174
+ installTools();
175
+ // First session: no policy. A tool call reads (and would have cached) [].
176
+ installInit(initBody());
177
+ const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
178
+ const client = makeClient({ enabled: true, onConfirm });
179
+ await client.initSession();
180
+ await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
181
+
182
+ // Second session adds a policy, then is cleared before any tool call reads it.
183
+ client.clearClientSession();
184
+ installInit(initBody({ webmcp: { requireConfirmFor: ["checkout_*"] } }));
185
+ await client.initSession();
186
+ client.clearClientSession();
187
+
188
+ await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
189
+
190
+ expect(onConfirm.mock.calls.map((c) => c[0]?.confirmRequired)).toEqual([undefined, true]);
191
+ });
192
+
193
+ it("flags every call while no client-token policy has loaded", async () => {
194
+ installTools();
195
+ const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
196
+ const client = makeClient({ enabled: true, onConfirm });
197
+
198
+ await client.executeWebMcpToolCall("webmcp:search", {});
199
+
200
+ expect(onConfirm.mock.calls[0]?.[0]).toMatchObject({ toolName: "search", confirmRequired: true });
201
+ });
202
+
203
+ it("leaves proxy mode (no client token) unflagged", async () => {
204
+ installTools();
205
+ const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
206
+ const client = new AgentWidgetClient({ apiUrl: API_URL, webmcp: { enabled: true, onConfirm } });
207
+
208
+ await client.executeWebMcpToolCall("webmcp:checkout_submit", {});
209
+
210
+ expect(onConfirm.mock.calls[0]?.[0]).not.toHaveProperty("confirmRequired");
211
+ });
212
+ });
@@ -18,25 +18,12 @@ import { createPopover, type PopoverHandle } from "../plugin-kit";
18
18
  import type { ComposerCustomAction, ComposerModel } from "../types";
19
19
  import { createElement, createNode } from "../utils/dom";
20
20
  import type { renderLucideIcon } from "../utils/icons";
21
- import { forwardMenuTokens } from "./composer-tokens";
22
21
 
23
22
  export const COMPOSER_MODEL_PICKER_ACTION_ID = "core:model";
24
23
  export const COMPOSER_MODEL_PICKER_ORDER = 700;
25
24
 
26
25
  const ROW_ICON_SIZE = 16;
27
26
 
28
- /**
29
- * Tokens the popover panel reads, forwarded onto the portaled panel on every
30
- * open; unset ones clear so the stylesheet's fallbacks stand.
31
- */
32
- const MENU_TOKEN_VARS = [
33
- "--persona-components-composer-modelPicker-menuBackground",
34
- "--persona-components-composer-modelPicker-menuBorderRadius",
35
- "--persona-components-composer-modelPicker-rowHoverBackground",
36
- "--persona-components-composer-modelPicker-labelColor",
37
- "--persona-components-composer-modelPicker-descriptionColor",
38
- ] as const;
39
-
40
27
  export interface ComposerModelPickerOptions {
41
28
  renderIcon: typeof renderLucideIcon;
42
29
  getModels: () => ReadonlyArray<ComposerModel>;
@@ -212,7 +199,6 @@ export function createComposerModelPickerAction(
212
199
  onDismiss: () => trigger.setAttribute("aria-expanded", "false"),
213
200
  });
214
201
  }
215
- forwardMenuTokens(trigger, menu, MENU_TOKEN_VARS);
216
202
  popover.open();
217
203
  trigger.setAttribute("aria-expanded", "true");
218
204
  const ids = [...rows.keys()];
@@ -22,7 +22,6 @@ import {
22
22
  COMPOSER_CONTROL_CLASS,
23
23
  COMPOSER_CONTROL_GLYPH_CLASS,
24
24
  COMPOSER_CONTROL_ICON_FALLBACK_PX,
25
- forwardMenuTokens,
26
25
  } from "./composer-tokens";
27
26
 
28
27
  export interface ComposerOverflowMenuOptions {
@@ -66,19 +65,6 @@ export interface ComposerOverflowMenu {
66
65
  const focusableSelector =
67
66
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
68
67
 
69
- /**
70
- * Panel surface tokens, forwarded onto the portaled panel on every open; unset
71
- * ones clear so the stylesheet's fallbacks stand.
72
- */
73
- const MENU_TOKEN_VARS = [
74
- "--persona-components-composer-overflowMenu-background",
75
- "--persona-components-composer-overflowMenu-borderColor",
76
- "--persona-components-composer-overflowMenu-borderRadius",
77
- "--persona-components-composer-overflowMenu-foreground",
78
- "--persona-components-composer-overflowMenu-shadow",
79
- ...["padding", "rowGap", "rowPadding", "rowBorderRadius", "fontSize", "lineHeight", "iconSize", "iconGap", "hoverBackground", "focusBackground", "descriptionColor", "descriptionFontSize", "dividerColor", "dividerSpacing", "minWidth", "maxHeight"].map((name) => `--persona-components-composer-overflowMenu-${name}`),
80
- ] as const;
81
-
82
68
  /**
83
69
  * Set on the menu row that holds keyboard focus. Rows that shade on
84
70
  * `:focus-visible` do not need it; wrapper rows around a live control do.
@@ -296,7 +282,6 @@ export function createComposerOverflowMenu(
296
282
  };
297
283
 
298
284
  const refresh = (): void => {
299
- forwardMenuTokens(triggerButton, panel, MENU_TOKEN_VARS);
300
285
  panel.setAttribute("data-persona-menu-description-layout", options.getConfig?.()?.descriptionLayout ?? "stacked");
301
286
  const width = options.getConfig?.()?.width;
302
287
  panel.style.width = width === "composer"
@@ -12,23 +12,3 @@ export const COMPOSER_CONTROL_GLYPH_CLASS = "persona-composer-control--glyph";
12
12
  export const COMPOSER_CONTROL_FALLBACK_PX = 40;
13
13
  /** Icon-size token default in CSS pixels, for the same JS-derived cases. */
14
14
  export const COMPOSER_CONTROL_ICON_FALLBACK_PX = 24;
15
-
16
- /**
17
- * Forward theme vars onto a portaled menu panel, which lives outside the
18
- * themed mount. The panel is reused across opens, so an unset var must clear
19
- * any previously stamped value for the stylesheet fallback to paint again.
20
- */
21
- export const forwardMenuTokens = (
22
- from: HTMLElement,
23
- to: HTMLElement,
24
- names: readonly string[]
25
- ): void => {
26
- const view = from.ownerDocument?.defaultView;
27
- if (!view) return;
28
- const computed = view.getComputedStyle(from);
29
- for (const name of names) {
30
- const value = computed.getPropertyValue(name).trim();
31
- if (value) to.style.setProperty(name, value);
32
- else to.style.removeProperty(name);
33
- }
34
- };
@@ -36,6 +36,12 @@ export type RuntypeExecutionStreamEvent = ({
36
36
  iterations?: number;
37
37
  kind: "agent" | "flow";
38
38
  outcome?: "completed" | "error" | "aborted";
39
+ output?: {
40
+ validation?: {
41
+ errored?: Array<string>;
42
+ failed?: Array<string>;
43
+ };
44
+ };
39
45
  seq: number;
40
46
  stepErrorCount?: number;
41
47
  stopReason?: string;
@@ -48,7 +54,12 @@ export type RuntypeExecutionStreamEvent = ({
48
54
  output: number;
49
55
  };
50
56
  totalTokensUsed?: number;
57
+ truncatedStepCount?: number;
51
58
  type: "execution_complete";
59
+ warnings?: Array<{
60
+ code: string;
61
+ message: string;
62
+ }>;
52
63
  }) | ({
53
64
  blockReason?: string;
54
65
  code?: string;
@@ -216,6 +227,7 @@ export type RuntypeExecutionStreamEvent = ({
216
227
  };
217
228
  success?: boolean;
218
229
  tokensUsed?: number;
230
+ truncated?: boolean;
219
231
  type: "step_complete";
220
232
  unresolvedVariables?: Array<string>;
221
233
  }) | {
@@ -439,6 +451,7 @@ export type RuntypeExecutionStreamEvent = ({
439
451
  toolCallId: string;
440
452
  type: "tool_output_delta";
441
453
  } | {
454
+ completedAt?: string;
442
455
  error?: string;
443
456
  executionId: string;
444
457
  executionTime?: number;