@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.
- package/dist/activity-ui.d.cts +3 -3
- package/dist/activity-ui.d.ts +3 -3
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/approval-ui.d.cts +3 -3
- package/dist/approval-ui.d.ts +3 -3
- package/dist/artifacts-ui.d.cts +3 -3
- package/dist/artifacts-ui.d.ts +3 -3
- package/dist/background-runtime.d.cts +1 -1
- package/dist/background-runtime.d.ts +1 -1
- package/dist/{chunk-CxNPSY-b.js → chunk-B1P8w9_X.js} +1 -1
- package/dist/{chunk-CV8EPIhT.js → chunk-B30Jdgg6.js} +1 -1
- package/dist/{chunk-6CUrH_jm.js → chunk-Bzo9GaAi.js} +1 -1
- package/dist/chunk-CNBMBx-Y.js +2 -0
- package/dist/chunk-Cd8mp8Sv.js +1 -0
- package/dist/chunk-CsfE8WRG.js +1 -0
- package/dist/{chunk-Co3VVwGe.js → chunk-D2pHRNEq.js} +0 -0
- package/dist/chunk-D3kZO8u-.js +1 -0
- package/dist/chunk-DAthf5H3.js +1 -0
- package/dist/{chunk-YMkMvAsa.js → chunk-DZ_aillY.js} +1 -1
- package/dist/{chunk-6C23QQdP.js → chunk-DgJV9zbK.js} +1 -1
- package/dist/chunk-Dv6KtIT1.js +1 -0
- package/dist/chunk-Dws8IrdR.js +1 -0
- package/dist/{chunk-DsWv3EGF.js → chunk-ThxxD8oK.js} +1 -1
- package/dist/client-dispatch.js +1 -1
- package/dist/client-history.js +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/composer-actions-runtime.cjs +1 -1
- package/dist/composer-actions-runtime.d.cts +3 -3
- package/dist/composer-actions-runtime.d.ts +3 -3
- package/dist/composer-actions-runtime.js +1 -1
- package/dist/{composer-input-i_FiUp6J.d.cts → composer-input-BSDNPOqV.d.cts} +1 -1
- package/dist/{composer-input-i_FiUp6J.d.ts → composer-input-BSDNPOqV.d.ts} +1 -1
- package/dist/{composer-store-DhGNgG7Z.d.cts → composer-store-DC-gAjtN.d.cts} +1 -1
- package/dist/{composer-store-DhGNgG7Z.d.ts → composer-store-DC-gAjtN.d.ts} +1 -1
- package/dist/{context-mention-manager-DL-xwGfL.d.cts → context-mention-manager-CCpeoGL2.d.cts} +2 -2
- package/dist/{context-mention-manager-DL-xwGfL.d.ts → context-mention-manager-CCpeoGL2.d.ts} +2 -2
- package/dist/context-mentions-inline.d.cts +2 -2
- package/dist/context-mentions-inline.d.ts +2 -2
- package/dist/context-mentions.cjs +1 -1
- package/dist/context-mentions.d.cts +3 -3
- package/dist/context-mentions.d.ts +3 -3
- package/dist/context-mentions.js +1 -1
- package/dist/{defaults-Bga2Xrsp.d.cts → defaults-tj5RcN8r.d.cts} +1 -1
- package/dist/{defaults-Bga2Xrsp.d.ts → defaults-tj5RcN8r.d.ts} +1 -1
- package/dist/dictation-runtime.d.cts +2 -2
- package/dist/dictation-runtime.d.ts +2 -2
- package/dist/event-stream-view.d.cts +1 -1
- package/dist/event-stream-view.d.ts +1 -1
- package/dist/{fallback-speech-engine-kLdyPcmk.d.cts → fallback-speech-engine-gkhbjLiZ.d.cts} +1 -1
- package/dist/{fallback-speech-engine-kLdyPcmk.d.ts → fallback-speech-engine-gkhbjLiZ.d.ts} +1 -1
- package/dist/{feature-preferences-CmwZGKrf.d.cts → feature-preferences-CerDWGZ7.d.cts} +1 -1
- package/dist/{feature-preferences-CmwZGKrf.d.ts → feature-preferences-CerDWGZ7.d.ts} +1 -1
- package/dist/forms-ui.d.cts +2 -2
- package/dist/forms-ui.d.ts +2 -2
- package/dist/{history-confirm-dialog-i46-lPN_.d.cts → history-confirm-dialog-CrkSB3Ra.d.cts} +1 -1
- package/dist/{history-confirm-dialog-i46-lPN_.d.ts → history-confirm-dialog-CrkSB3Ra.d.ts} +1 -1
- package/dist/history-shell.d.cts +6 -6
- package/dist/history-shell.d.ts +6 -6
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/index.cjs +32 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +40 -40
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/launcher.global.js +1 -1
- package/dist/{loading-label-runtime-loader-Ccr3ETd3.d.cts → loading-label-runtime-loader-fWIRrhUF.d.cts} +1 -1
- package/dist/{loading-label-runtime-loader-Ccr3ETd3.d.ts → loading-label-runtime-loader-fWIRrhUF.d.ts} +1 -1
- package/dist/loading-label-runtime.d.cts +1 -1
- package/dist/loading-label-runtime.d.ts +1 -1
- package/dist/{message-bubble-C6AVaH7f.d.cts → message-bubble-Czc0lOHr.d.cts} +1 -1
- package/dist/{message-bubble-C6AVaH7f.d.ts → message-bubble-Czc0lOHr.d.ts} +1 -1
- package/dist/{plugin-kit-BVggqE3C.d.cts → plugin-kit-C30jE-cw.d.cts} +3 -1
- package/dist/{plugin-kit-BVggqE3C.d.ts → plugin-kit-C30jE-cw.d.ts} +3 -1
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +1 -1
- package/dist/plugin-kit.d.ts +1 -1
- package/dist/plugin-kit.js +1 -1
- package/dist/preferences-runtime.d.cts +1 -1
- package/dist/preferences-runtime.d.ts +1 -1
- package/dist/{preview-BMdRffo4.d.cts → preview-D-osV3FU.d.cts} +2 -2
- package/dist/{preview-BMdRffo4.d.ts → preview-D-osV3FU.d.ts} +2 -2
- package/dist/{registry-B16l8s1b.d.cts → registry-B6w91tx0.d.cts} +1 -1
- package/dist/{registry-B16l8s1b.d.ts → registry-B6w91tx0.d.ts} +1 -1
- package/dist/{session-CtZHe7fc.d.cts → session-BUQrs3Of.d.cts} +10 -9
- package/dist/{session-CtZHe7fc.d.ts → session-BUQrs3Of.d.ts} +10 -9
- package/dist/session-reconnect.d.cts +2 -2
- package/dist/session-reconnect.d.ts +2 -2
- package/dist/smart-dom-reader.d.cts +1 -1
- package/dist/smart-dom-reader.d.ts +1 -1
- package/dist/theme-editor-preview.cjs +43 -43
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +3 -3
- package/dist/theme-editor.d.cts +2 -2
- package/dist/theme-editor.d.ts +2 -2
- package/dist/theme-reference.d.cts +2 -2
- package/dist/theme-reference.d.ts +2 -2
- package/dist/{tool-loading-animation-D2cyFvmM.d.cts → tool-loading-animation-B7XUf8TN.d.cts} +1 -1
- package/dist/{tool-loading-animation-D2cyFvmM.d.ts → tool-loading-animation-B7XUf8TN.d.ts} +1 -1
- package/dist/{turn-activity-ui-Do1Nd-Ow.d.cts → turn-activity-ui-CohuNMHT.d.cts} +2 -2
- package/dist/{turn-activity-ui-Do1Nd-Ow.d.ts → turn-activity-ui-CohuNMHT.d.ts} +2 -2
- package/dist/turn-activity-ui.d.cts +1 -1
- package/dist/turn-activity-ui.d.ts +1 -1
- package/dist/{types-BC7MRxO5.d.cts → types-9i0xcAMT.d.cts} +52 -0
- package/dist/{types-BC7MRxO5.d.ts → types-9i0xcAMT.d.ts} +52 -0
- package/dist/voice-client-tools.d.cts +1 -1
- package/dist/voice-client-tools.d.ts +1 -1
- package/dist/voice-runtime.cjs +1 -1
- package/dist/voice-runtime.d.cts +2 -2
- package/dist/voice-runtime.d.ts +2 -2
- package/dist/voice-runtime.js +1 -1
- package/dist/voice-worklet-player.d.cts +2 -2
- package/dist/voice-worklet-player.d.ts +2 -2
- package/dist/{webmcp-runtime-entry-B8g46vS0.d.ts → webmcp-runtime-entry-XNaXa-9h.d.cts} +9 -1
- package/dist/{webmcp-runtime-entry-B8g46vS0.d.cts → webmcp-runtime-entry-XNaXa-9h.d.ts} +9 -1
- package/dist/webmcp-runtime.cjs +1 -1
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/webmcp-runtime.js +1 -1
- package/package.json +2 -2
- package/src/client-dispatch.ts +13 -0
- package/src/client.ts +37 -2
- package/src/client.webmcp-require-confirm.test.ts +212 -0
- package/src/components/composer-model-picker.ts +0 -14
- package/src/components/composer-overflow-menu.ts +0 -15
- package/src/components/composer-tokens.ts +0 -20
- package/src/generated/runtype-openapi-contract.ts +13 -0
- package/src/plugin-kit.test.ts +101 -0
- package/src/plugin-kit.ts +29 -1
- package/src/session.ts +6 -2
- package/src/session.webmcp.test.ts +32 -0
- package/src/types.ts +35 -0
- package/src/ui.composer-models.test.ts +5 -7
- package/src/ui.composer-overflow.test.ts +14 -21
- package/src/utils/context-mention-controller.test.ts +56 -0
- package/src/webmcp-bridge.test.ts +102 -0
- package/src/webmcp-runtime-entry.ts +29 -1
- package/dist/chunk-BAkaTZpd.js +0 -1
- package/dist/chunk-BEDTp-dm.js +0 -1
- package/dist/chunk-C-rux8Q9.js +0 -1
- package/dist/chunk-CNhThrLX.js +0 -1
- package/dist/chunk-CzLEhoB_.js +0 -2
- package/dist/chunk-Dqa_GOcj.js +0 -1
- 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-
|
|
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;
|
package/dist/webmcp-runtime.cjs
CHANGED
|
@@ -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.#
|
|
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;
|
package/dist/webmcp-runtime.d.ts
CHANGED
package/dist/webmcp-runtime.js
CHANGED
|
@@ -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.#
|
|
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.
|
|
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": "
|
|
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",
|
package/src/client-dispatch.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|