@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
package/src/plugin-kit.test.ts
CHANGED
|
@@ -262,6 +262,107 @@ describe("createPopover", () => {
|
|
|
262
262
|
});
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
+
describe("createPopover theme carry-over", () => {
|
|
266
|
+
const setupRoot = () => {
|
|
267
|
+
const root = document.createElement("div");
|
|
268
|
+
root.setAttribute("data-persona-root", "true");
|
|
269
|
+
root.setAttribute("data-persona-color-scheme", "dark");
|
|
270
|
+
root.setAttribute("data-persona-defaults", "v5");
|
|
271
|
+
root.style.setProperty("--persona-surface", "#17181c");
|
|
272
|
+
root.style.setProperty("--persona-border", "#2e3038");
|
|
273
|
+
root.style.setProperty("--persona-text", "#f4f4f5");
|
|
274
|
+
root.style.height = "100%";
|
|
275
|
+
const anchor = document.createElement("button");
|
|
276
|
+
root.appendChild(anchor);
|
|
277
|
+
document.body.appendChild(root);
|
|
278
|
+
const content = document.createElement("div");
|
|
279
|
+
return { root, anchor, content };
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
it("copies the root's theme variables and scheme onto body-mounted content", () => {
|
|
283
|
+
const { anchor, content } = setupRoot();
|
|
284
|
+
createPopover({ anchor, content }).open();
|
|
285
|
+
|
|
286
|
+
expect(content.parentElement).toBe(document.body);
|
|
287
|
+
expect(content.style.getPropertyValue("--persona-surface")).toBe("#17181c");
|
|
288
|
+
expect(content.style.getPropertyValue("--persona-border")).toBe("#2e3038");
|
|
289
|
+
expect(content.style.getPropertyValue("--persona-text")).toBe("#f4f4f5");
|
|
290
|
+
expect(content.getAttribute("data-persona-color-scheme")).toBe("dark");
|
|
291
|
+
expect(content.getAttribute("data-persona-defaults")).toBe("v5");
|
|
292
|
+
// Only custom properties travel, never the root's own layout.
|
|
293
|
+
expect(content.style.height).toBe("");
|
|
294
|
+
// Not a second widget root: root-scoped rules and lookups stay unambiguous.
|
|
295
|
+
expect(content.hasAttribute("data-persona-root")).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("follows a live scheme change while open and clears removed variables", async () => {
|
|
299
|
+
const { root, anchor, content } = setupRoot();
|
|
300
|
+
createPopover({ anchor, content }).open();
|
|
301
|
+
|
|
302
|
+
root.style.setProperty("--persona-surface", "#ffffff");
|
|
303
|
+
root.style.removeProperty("--persona-border");
|
|
304
|
+
root.setAttribute("data-persona-color-scheme", "light");
|
|
305
|
+
await flushMicrotasks();
|
|
306
|
+
|
|
307
|
+
expect(content.style.getPropertyValue("--persona-surface")).toBe("#ffffff");
|
|
308
|
+
expect(content.style.getPropertyValue("--persona-border")).toBe("");
|
|
309
|
+
expect(content.getAttribute("data-persona-color-scheme")).toBe("light");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("stops following the root once closed and refreshes on the next open", async () => {
|
|
313
|
+
const { root, anchor, content } = setupRoot();
|
|
314
|
+
const popover = createPopover({ anchor, content });
|
|
315
|
+
popover.open();
|
|
316
|
+
popover.close();
|
|
317
|
+
|
|
318
|
+
root.style.setProperty("--persona-surface", "#ffffff");
|
|
319
|
+
await flushMicrotasks();
|
|
320
|
+
expect(content.style.getPropertyValue("--persona-surface")).toBe("#17181c");
|
|
321
|
+
|
|
322
|
+
popover.open();
|
|
323
|
+
expect(content.style.getPropertyValue("--persona-surface")).toBe("#ffffff");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("leaves content mounted inside the root to plain inheritance", () => {
|
|
327
|
+
const { root, anchor, content } = setupRoot();
|
|
328
|
+
createPopover({ anchor, content, container: root }).open();
|
|
329
|
+
|
|
330
|
+
expect(content.parentElement).toBe(root);
|
|
331
|
+
expect(content.style.getPropertyValue("--persona-surface")).toBe("");
|
|
332
|
+
expect(content.hasAttribute("data-persona-color-scheme")).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("carries the theme into a shadow root, where content sits beside the root", () => {
|
|
336
|
+
const host = document.createElement("div");
|
|
337
|
+
document.body.appendChild(host);
|
|
338
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
339
|
+
const root = document.createElement("div");
|
|
340
|
+
root.setAttribute("data-persona-root", "true");
|
|
341
|
+
root.setAttribute("data-persona-color-scheme", "dark");
|
|
342
|
+
root.style.setProperty("--persona-surface", "#17181c");
|
|
343
|
+
const anchor = document.createElement("button");
|
|
344
|
+
root.appendChild(anchor);
|
|
345
|
+
shadow.appendChild(root);
|
|
346
|
+
const content = document.createElement("div");
|
|
347
|
+
|
|
348
|
+
createPopover({ anchor, content }).open();
|
|
349
|
+
|
|
350
|
+
expect(content.parentNode).toBe(shadow);
|
|
351
|
+
expect(content.style.getPropertyValue("--persona-surface")).toBe("#17181c");
|
|
352
|
+
expect(content.getAttribute("data-persona-color-scheme")).toBe("dark");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("does nothing for an anchor outside any widget root", () => {
|
|
356
|
+
const anchor = document.createElement("button");
|
|
357
|
+
document.body.appendChild(anchor);
|
|
358
|
+
const content = document.createElement("div");
|
|
359
|
+
createPopover({ anchor, content }).open();
|
|
360
|
+
|
|
361
|
+
expect(content.style.cssText).not.toContain("--persona");
|
|
362
|
+
expect(content.hasAttribute("data-persona-color-scheme")).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
265
366
|
describe("viewport-constrained popovers", () => {
|
|
266
367
|
const rect = (left: number, top: number, width: number, height: number): DOMRect =>
|
|
267
368
|
({ left, top, right: left + width, bottom: top + height, width, height }) as DOMRect;
|
package/src/plugin-kit.ts
CHANGED
|
@@ -156,7 +156,9 @@ export interface PopoverOptions {
|
|
|
156
156
|
/**
|
|
157
157
|
* Where to mount `content`. Defaults to the anchor's shadow root (when
|
|
158
158
|
* shadowed), its open dialog, or `document.body`: keeping it inside the same style + stacking
|
|
159
|
-
* scope as the anchor while escaping the transcript's scroll clipping.
|
|
159
|
+
* scope as the anchor while escaping the transcript's scroll clipping. Mounted
|
|
160
|
+
* outside the anchor's `[data-persona-root]`, `content` carries a copy of the
|
|
161
|
+
* root's theme variables and color scheme, kept current while open.
|
|
160
162
|
*/
|
|
161
163
|
container?: HTMLElement | ShadowRoot;
|
|
162
164
|
onOpen?: () => void;
|
|
@@ -216,6 +218,24 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
|
|
216
218
|
const currentContainer = () => options.container ?? defaultContainer(anchor);
|
|
217
219
|
let open = false;
|
|
218
220
|
let detach: (() => void) | null = null;
|
|
221
|
+
let carried: string[] = [];
|
|
222
|
+
|
|
223
|
+
// The theme lives on the widget root as inline custom properties plus scheme
|
|
224
|
+
// attributes. Content mounted outside that root (body, dialog, shadow root)
|
|
225
|
+
// would paint the stylesheet's light fallbacks, so it carries a copy.
|
|
226
|
+
const carryTheme = (): void => {
|
|
227
|
+
const root = anchor.closest<HTMLElement>("[data-persona-root]");
|
|
228
|
+
if (!root || root.contains(content)) return;
|
|
229
|
+
const { style } = root;
|
|
230
|
+
for (const name of carried) if (!style.getPropertyValue(name)) content.style.removeProperty(name);
|
|
231
|
+
carried = Array.from(style).filter((name) => name.startsWith("--"));
|
|
232
|
+
for (const name of carried) content.style.setProperty(name, style.getPropertyValue(name));
|
|
233
|
+
for (const attr of ["data-persona-color-scheme", "data-persona-defaults"]) {
|
|
234
|
+
const value = root.getAttribute(attr);
|
|
235
|
+
if (value) content.setAttribute(attr, value);
|
|
236
|
+
else content.removeAttribute(attr);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
219
239
|
|
|
220
240
|
const reposition = (): void => {
|
|
221
241
|
if (!open) return;
|
|
@@ -224,6 +244,7 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
|
|
224
244
|
const focused = content.contains(content.ownerDocument.activeElement)
|
|
225
245
|
? content.ownerDocument.activeElement as HTMLElement : null;
|
|
226
246
|
container.appendChild(content);
|
|
247
|
+
carryTheme();
|
|
227
248
|
focused?.focus({ preventScroll: true });
|
|
228
249
|
}
|
|
229
250
|
const rect = options.getAnchorRect?.() ?? anchor.getBoundingClientRect();
|
|
@@ -321,9 +342,16 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
|
|
321
342
|
ownerWindow.addEventListener("resize", onReposition);
|
|
322
343
|
const moveRoot = anchor.closest("[data-persona-root]");
|
|
323
344
|
moveRoot?.addEventListener("persona:internal:host-moved", onReposition);
|
|
345
|
+
// A live theme or color-scheme change rewrites the root while open.
|
|
346
|
+
let themeObserver: MutationObserver | undefined;
|
|
347
|
+
if (moveRoot && ownerWindow.MutationObserver) {
|
|
348
|
+
themeObserver = new ownerWindow.MutationObserver(carryTheme);
|
|
349
|
+
themeObserver.observe(moveRoot, { attributeFilter: ["style", "data-persona-color-scheme"] });
|
|
350
|
+
}
|
|
324
351
|
|
|
325
352
|
detach = () => {
|
|
326
353
|
observer?.disconnect();
|
|
354
|
+
themeObserver?.disconnect();
|
|
327
355
|
ownerWindow.clearTimeout(armId);
|
|
328
356
|
ownerDocument.removeEventListener("pointerdown", onOutside, true);
|
|
329
357
|
ownerWindow.removeEventListener("scroll", onReposition, true);
|
package/src/session.ts
CHANGED
|
@@ -3305,9 +3305,13 @@ export class AgentWidgetSession {
|
|
|
3305
3305
|
public requestWebMcpApproval(info: WebMcpConfirmInfo): Promise<boolean> {
|
|
3306
3306
|
// Per-tool policy hook: auto-allow opted-out tools without any UI. A
|
|
3307
3307
|
// throwing predicate must not block the call, so fall through to an
|
|
3308
|
-
// explicit gate on error.
|
|
3308
|
+
// explicit gate on error. A `requireConfirmFor` match (flagged by the
|
|
3309
|
+
// bridge) never consults it: the surface owner said it must be confirmed.
|
|
3309
3310
|
try {
|
|
3310
|
-
if (
|
|
3311
|
+
if (
|
|
3312
|
+
info.confirmRequired !== true &&
|
|
3313
|
+
this.config.webmcp?.autoApprove?.(info) === true
|
|
3314
|
+
) {
|
|
3311
3315
|
return Promise.resolve(true);
|
|
3312
3316
|
}
|
|
3313
3317
|
} catch {
|
|
@@ -1230,3 +1230,35 @@ describe("AgentWidgetSession: WebMCP parallel batched resume (core#3878)", () =>
|
|
|
1230
1230
|
expect(s.webMcpApprovalResolvers.size).toBe(0);
|
|
1231
1231
|
});
|
|
1232
1232
|
});
|
|
1233
|
+
|
|
1234
|
+
describe("WebMCP requireConfirmFor on the default approval UI", () => {
|
|
1235
|
+
const pendingBubbles = (session: AgentWidgetSession) =>
|
|
1236
|
+
(session as unknown as { messages: AgentWidgetMessage[] }).messages.filter(
|
|
1237
|
+
(m) => m.variant === "approval" && m.approval?.status === "pending",
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
it("prompts for a matched tool even when autoApprove returns true", async () => {
|
|
1241
|
+
const autoApprove = vi.fn(() => true);
|
|
1242
|
+
const { session } = makeSession({ webmcp: { autoApprove } });
|
|
1243
|
+
const pending = session.requestWebMcpApproval({
|
|
1244
|
+
toolName: "checkout_submit",
|
|
1245
|
+
args: {},
|
|
1246
|
+
reason: "gate",
|
|
1247
|
+
confirmRequired: true,
|
|
1248
|
+
});
|
|
1249
|
+
expect(autoApprove).not.toHaveBeenCalled();
|
|
1250
|
+
expect(pendingBubbles(session)).toHaveLength(1);
|
|
1251
|
+
session.cancel();
|
|
1252
|
+
await expect(pending).resolves.toBe(false);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it("still auto-approves an unmatched tool", async () => {
|
|
1256
|
+
const autoApprove = vi.fn(() => true);
|
|
1257
|
+
const { session } = makeSession({ webmcp: { autoApprove } });
|
|
1258
|
+
await expect(
|
|
1259
|
+
session.requestWebMcpApproval({ toolName: "search", args: {}, reason: "gate" }),
|
|
1260
|
+
).resolves.toBe(true);
|
|
1261
|
+
expect(autoApprove).toHaveBeenCalledTimes(1);
|
|
1262
|
+
expect(pendingBubbles(session)).toHaveLength(0);
|
|
1263
|
+
});
|
|
1264
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -886,6 +886,16 @@ export type WebMcpConfirmInfo = {
|
|
|
886
886
|
* callbacks on v6 receive an AbortSignal, not a nested confirmation API.
|
|
887
887
|
*/
|
|
888
888
|
reason: 'gate';
|
|
889
|
+
/**
|
|
890
|
+
* `true` when the tool matches a `requireConfirmFor` pattern (the widget's
|
|
891
|
+
* `webmcp.requireConfirmFor`, or the bound Runtype surface's
|
|
892
|
+
* `behavior.webmcp.requireConfirmFor` from client-token init). The call
|
|
893
|
+
* must be confirmed by the user: Persona never runs `autoApprove` for it,
|
|
894
|
+
* and a custom `onConfirm` should not auto-approve it either. In
|
|
895
|
+
* client-token mode it is also set on every call until the surface policy
|
|
896
|
+
* has loaded (fail closed).
|
|
897
|
+
*/
|
|
898
|
+
confirmRequired?: true;
|
|
889
899
|
/**
|
|
890
900
|
* Aborts when the call no longer needs an answer (its deadline passed, or it
|
|
891
901
|
* was cancelled). A confirm UI should close and resolve `false`; the tool
|
|
@@ -936,8 +946,23 @@ export type AgentWidgetWebMcpConfig = {
|
|
|
936
946
|
* confirmation UI entirely. Use this to auto-allow read-only tools (e.g.
|
|
937
947
|
* a catalog search) while still gating mutating ones. Only consulted on
|
|
938
948
|
* the default-UI path: a custom `onConfirm` takes full control instead.
|
|
949
|
+
* Never consulted for a tool matching `requireConfirmFor`.
|
|
939
950
|
*/
|
|
940
951
|
autoApprove?: (info: WebMcpConfirmInfo) => boolean;
|
|
952
|
+
/**
|
|
953
|
+
* Tool-name patterns that must always be confirmed, even when `autoApprove`
|
|
954
|
+
* would approve them (and even on the voice path's read-only fast tier).
|
|
955
|
+
* Matched against the bare tool name (no `webmcp:` prefix); `*` matches any
|
|
956
|
+
* run of characters, e.g. `["checkout_*", "delete_account"]`. A matched
|
|
957
|
+
* call shows the default approval bubble, or reaches your `onConfirm` with
|
|
958
|
+
* `info.confirmRequired === true`.
|
|
959
|
+
*
|
|
960
|
+
* Merged with the bound surface's `behavior.webmcp.requireConfirmFor`,
|
|
961
|
+
* which Runtype returns from `/v1/client/init` in client-token mode. This
|
|
962
|
+
* is client-side UX, not a security boundary: the server enforces only
|
|
963
|
+
* `webmcp.enabled` and its `allowlist`.
|
|
964
|
+
*/
|
|
965
|
+
requireConfirmFor?: string[];
|
|
941
966
|
/**
|
|
942
967
|
* Confirm gate handler. When omitted, Persona renders its native in-panel
|
|
943
968
|
* approval bubble (the same chrome used for server-driven tool approvals)
|
|
@@ -6822,6 +6847,11 @@ export type ClientSession = {
|
|
|
6822
6847
|
welcomeMessage: string | null;
|
|
6823
6848
|
placeholder: string;
|
|
6824
6849
|
theme: Record<string, unknown> | null;
|
|
6850
|
+
/**
|
|
6851
|
+
* The bound chat surface's WebMCP confirm policy. Present only when the
|
|
6852
|
+
* surface enables WebMCP and lists `requireConfirmFor` patterns.
|
|
6853
|
+
*/
|
|
6854
|
+
webmcp?: { requireConfirmFor: string[] };
|
|
6825
6855
|
};
|
|
6826
6856
|
};
|
|
6827
6857
|
|
|
@@ -6862,6 +6892,11 @@ export type ClientInitResponse = {
|
|
|
6862
6892
|
welcomeMessage: string | null;
|
|
6863
6893
|
placeholder: string;
|
|
6864
6894
|
theme: Record<string, unknown> | null;
|
|
6895
|
+
/**
|
|
6896
|
+
* The bound chat surface's WebMCP confirm policy. Present only when the
|
|
6897
|
+
* surface enables WebMCP and lists `requireConfirmFor` patterns.
|
|
6898
|
+
*/
|
|
6899
|
+
webmcp?: { requireConfirmFor: string[] };
|
|
6865
6900
|
};
|
|
6866
6901
|
};
|
|
6867
6902
|
|
|
@@ -344,29 +344,27 @@ describe("composer model picker", () => {
|
|
|
344
344
|
},
|
|
345
345
|
});
|
|
346
346
|
expect(mount.style.getPropertyValue(token)).toBe("#1e1f20");
|
|
347
|
-
// The panel is portaled to document.body,
|
|
348
|
-
// mount's theme vars.
|
|
349
|
-
// computed style does not inherit custom properties.
|
|
347
|
+
// The panel is portaled to document.body, outside the mount, so it
|
|
348
|
+
// carries a copy of the mount's theme vars.
|
|
350
349
|
const button = trigger(mount)!;
|
|
351
|
-
button.style.setProperty(token, "#1e1f20");
|
|
352
350
|
|
|
353
351
|
button.click();
|
|
354
352
|
const panel = menu()!;
|
|
353
|
+
expect(mount.contains(panel)).toBe(false);
|
|
355
354
|
expect(panel.style.getPropertyValue(token)).toBe("#1e1f20");
|
|
356
355
|
button.click();
|
|
357
356
|
|
|
358
357
|
// Explicit undefined is the documented reset, so the mount stops
|
|
359
|
-
// carrying the var
|
|
358
|
+
// carrying the var.
|
|
360
359
|
controller.update({
|
|
361
360
|
theme: {
|
|
362
361
|
components: { composer: { modelPicker: { menuBackground: undefined } } },
|
|
363
362
|
},
|
|
364
363
|
});
|
|
365
364
|
expect(mount.style.getPropertyValue(token)).toBe("");
|
|
366
|
-
button.style.removeProperty(token);
|
|
367
365
|
|
|
368
366
|
// The menu outlives the update, so the next open has to clear the value
|
|
369
|
-
// the first one
|
|
367
|
+
// the first one copied; an empty inline value is the fallback state.
|
|
370
368
|
trigger(mount)!.click();
|
|
371
369
|
expect(menu()).toBe(panel);
|
|
372
370
|
expect(panel.style.getPropertyValue(token)).toBe("");
|
|
@@ -103,27 +103,25 @@ describe("composer overflow menu in the live widget", () => {
|
|
|
103
103
|
vi.restoreAllMocks();
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it("
|
|
106
|
+
it("carries the panel surface tokens onto the portaled menu on open", () => {
|
|
107
107
|
const { mount } = makeController({
|
|
108
108
|
composer: {
|
|
109
109
|
actions: [action("folded", { presentation: "overflow" })],
|
|
110
110
|
actionOverflow: { enabled: true },
|
|
111
111
|
},
|
|
112
|
+
theme: {
|
|
113
|
+
components: {
|
|
114
|
+
composer: {
|
|
115
|
+
overflowMenu: { background: "#353535", borderColor: "rgba(255, 255, 255, 0.08)" },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
112
119
|
});
|
|
113
|
-
// The panel is portaled to document.body, so it never inherits the mount's
|
|
114
|
-
// theme vars. Stamped on the trigger here because jsdom's computed style
|
|
115
|
-
// does not inherit custom properties.
|
|
116
|
-
const trigger = triggerOf(mount)!;
|
|
117
|
-
trigger.style.setProperty(
|
|
118
|
-
"--persona-components-composer-overflowMenu-background",
|
|
119
|
-
"#353535"
|
|
120
|
-
);
|
|
121
|
-
trigger.style.setProperty(
|
|
122
|
-
"--persona-components-composer-overflowMenu-borderColor",
|
|
123
|
-
"rgba(255, 255, 255, 0.08)"
|
|
124
|
-
);
|
|
125
120
|
|
|
121
|
+
// The panel is portaled to document.body, outside the mount, so it
|
|
122
|
+
// carries a copy of the mount's theme vars.
|
|
126
123
|
const panel = openPanel(mount)!;
|
|
124
|
+
expect(mount.contains(panel)).toBe(false);
|
|
127
125
|
|
|
128
126
|
expect(
|
|
129
127
|
panel.style.getPropertyValue(
|
|
@@ -135,7 +133,7 @@ describe("composer overflow menu in the live widget", () => {
|
|
|
135
133
|
"--persona-components-composer-overflowMenu-borderColor"
|
|
136
134
|
)
|
|
137
135
|
).toBe("rgba(255, 255, 255, 0.08)");
|
|
138
|
-
// Unset keys
|
|
136
|
+
// Unset keys copy nothing, so the stylesheet's fallback still stands.
|
|
139
137
|
expect(
|
|
140
138
|
panel.style.getPropertyValue(
|
|
141
139
|
"--persona-components-composer-overflowMenu-shadow"
|
|
@@ -155,27 +153,22 @@ describe("composer overflow menu in the live widget", () => {
|
|
|
155
153
|
},
|
|
156
154
|
});
|
|
157
155
|
expect(mount.style.getPropertyValue(token)).toBe("#353535");
|
|
158
|
-
// Stamped on the trigger for the same reason as the test above: jsdom's
|
|
159
|
-
// computed style does not inherit custom properties.
|
|
160
|
-
const trigger = triggerOf(mount)!;
|
|
161
|
-
trigger.style.setProperty(token, "#353535");
|
|
162
156
|
|
|
163
157
|
const panel = openPanel(mount)!;
|
|
164
158
|
expect(panel.style.getPropertyValue(token)).toBe("#353535");
|
|
165
159
|
triggerOf(mount)!.click();
|
|
166
160
|
|
|
167
161
|
// Explicit undefined is the documented reset, so the mount stops carrying
|
|
168
|
-
// the var
|
|
162
|
+
// the var.
|
|
169
163
|
controller.update({
|
|
170
164
|
theme: {
|
|
171
165
|
components: { composer: { overflowMenu: { background: undefined } } },
|
|
172
166
|
},
|
|
173
167
|
});
|
|
174
168
|
expect(mount.style.getPropertyValue(token)).toBe("");
|
|
175
|
-
trigger.style.removeProperty(token);
|
|
176
169
|
|
|
177
170
|
// The panel outlives the update, so the next open has to clear the value
|
|
178
|
-
// the first one
|
|
171
|
+
// the first one copied; an empty inline value is the fallback state.
|
|
179
172
|
const reopened = openPanel(mount)!;
|
|
180
173
|
expect(reopened).toBe(panel);
|
|
181
174
|
expect(reopened.style.getPropertyValue(token)).toBe("");
|
|
@@ -5,6 +5,7 @@ import { ContextMentionController } from "./context-mention-controller";
|
|
|
5
5
|
import { createTextareaComposerInput } from "./composer-input";
|
|
6
6
|
import { createContentEditableComposerInput } from "./composer-contenteditable";
|
|
7
7
|
import { createSlashCommandsSource } from "./mention-matcher";
|
|
8
|
+
import { applyThemeVariables } from "./theme";
|
|
8
9
|
import type {
|
|
9
10
|
AgentWidgetConfig,
|
|
10
11
|
AgentWidgetContextMentionConfig,
|
|
@@ -433,6 +434,61 @@ describe("ContextMentionController", () => {
|
|
|
433
434
|
});
|
|
434
435
|
});
|
|
435
436
|
|
|
437
|
+
describe("ContextMentionController — theme", () => {
|
|
438
|
+
beforeEach(() => {
|
|
439
|
+
document.body.innerHTML = "";
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// The menu portals to document.body, outside the themed widget root. Without
|
|
443
|
+
// the root's variables it painted the light fallbacks in a dark widget.
|
|
444
|
+
it("paints the body-portaled menu with the widget's dark theme and follows a scheme change", async () => {
|
|
445
|
+
const root = document.createElement("div");
|
|
446
|
+
root.setAttribute("data-persona-root", "true");
|
|
447
|
+
const dark: AgentWidgetConfig = {
|
|
448
|
+
colorScheme: "dark",
|
|
449
|
+
future: { v5Defaults: true },
|
|
450
|
+
theme: { semantic: { colors: { surface: "#17181c", border: "#2e3038" } } },
|
|
451
|
+
} as AgentWidgetConfig;
|
|
452
|
+
applyThemeVariables(root, dark);
|
|
453
|
+
const form = document.createElement("form");
|
|
454
|
+
const textarea = document.createElement("textarea");
|
|
455
|
+
form.appendChild(textarea);
|
|
456
|
+
root.appendChild(form);
|
|
457
|
+
document.body.appendChild(root);
|
|
458
|
+
|
|
459
|
+
const controller = new ContextMentionController({
|
|
460
|
+
mentionConfig: { enabled: true, sources: [syncSource("files", [item("App.tsx")])] },
|
|
461
|
+
composerInput: createTextareaComposerInput(textarea),
|
|
462
|
+
anchor: form,
|
|
463
|
+
getMessages: () => [],
|
|
464
|
+
getConfig: () => ({}) as AgentWidgetConfig,
|
|
465
|
+
onSelect: vi.fn(() => true),
|
|
466
|
+
announce: vi.fn(),
|
|
467
|
+
});
|
|
468
|
+
textarea.value = "@";
|
|
469
|
+
textarea.setSelectionRange(1, 1);
|
|
470
|
+
controller.onInput();
|
|
471
|
+
|
|
472
|
+
const menu = document.querySelector<HTMLElement>("[data-persona-mention-menu]")!;
|
|
473
|
+
expect(menu.parentElement).toBe(document.body);
|
|
474
|
+
expect(menu.getAttribute("data-persona-color-scheme")).toBe("dark");
|
|
475
|
+
expect(menu.style.getPropertyValue("--persona-surface")).toBe("#17181c");
|
|
476
|
+
expect(menu.style.getPropertyValue("--persona-border")).toBe("#2e3038");
|
|
477
|
+
const darkText = root.style.getPropertyValue("--persona-text");
|
|
478
|
+
expect(darkText).not.toBe("");
|
|
479
|
+
expect(menu.style.getPropertyValue("--persona-text")).toBe(darkText);
|
|
480
|
+
|
|
481
|
+
// controller.update({ colorScheme }) re-applies the theme onto the root.
|
|
482
|
+
applyThemeVariables(root, { ...dark, colorScheme: "light" } as AgentWidgetConfig);
|
|
483
|
+
await tick();
|
|
484
|
+
expect(menu.getAttribute("data-persona-color-scheme")).toBe("light");
|
|
485
|
+
expect(menu.style.getPropertyValue("--persona-text")).toBe(root.style.getPropertyValue("--persona-text"));
|
|
486
|
+
expect(menu.style.getPropertyValue("--persona-text")).not.toBe(darkText);
|
|
487
|
+
|
|
488
|
+
controller.destroy();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
436
492
|
describe("ContextMentionController — slash-commands", () => {
|
|
437
493
|
beforeEach(() => {
|
|
438
494
|
document.body.innerHTML = "";
|
|
@@ -30,6 +30,7 @@ vi.mock("@mcp-b/webmcp-polyfill", () => ({
|
|
|
30
30
|
// one-argument constructor, wired to the real core deps — so these tests also
|
|
31
31
|
// exercise the deps-injection seam the lazy CDN path uses.
|
|
32
32
|
import { WebMcpBridge } from "./webmcp-bridge-public";
|
|
33
|
+
import { WebMcpBridge as WebMcpBridgeRuntime } from "./webmcp-runtime-entry";
|
|
33
34
|
import {
|
|
34
35
|
getWebMcpToolDisplayTitle,
|
|
35
36
|
isWebMcpToolName,
|
|
@@ -962,3 +963,104 @@ describe("WebMcpBridge.watchToolChanges", () => {
|
|
|
962
963
|
expect(() => stop()).not.toThrow();
|
|
963
964
|
});
|
|
964
965
|
});
|
|
966
|
+
|
|
967
|
+
describe("WebMcpBridge requireConfirmFor", () => {
|
|
968
|
+
const runtimeBridge = (
|
|
969
|
+
config: AgentWidgetWebMcpConfig,
|
|
970
|
+
getRequireConfirmFor: () => readonly string[] | null | undefined,
|
|
971
|
+
) =>
|
|
972
|
+
new WebMcpBridgeRuntime(config, {
|
|
973
|
+
recordToolDisplayTitles: () => {},
|
|
974
|
+
getToolDisplayTitle: () => undefined,
|
|
975
|
+
loadPolyfill: async () => ({ initializeWebMCPPolyfill: () => {} }),
|
|
976
|
+
getRequireConfirmFor,
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it("flags a widget-config match with confirmRequired on the confirm info", async () => {
|
|
980
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
981
|
+
registry.tools = [fakeTool({ name: "checkout_submit" }), fakeTool({ name: "search" })];
|
|
982
|
+
const bridge = new WebMcpBridge({
|
|
983
|
+
enabled: true,
|
|
984
|
+
onConfirm,
|
|
985
|
+
requireConfirmFor: ["checkout_*"],
|
|
986
|
+
});
|
|
987
|
+
await bridge.executeToolCall("webmcp:checkout_submit", {});
|
|
988
|
+
await bridge.executeToolCall("webmcp:search", {});
|
|
989
|
+
expect(onConfirm).toHaveBeenNthCalledWith(
|
|
990
|
+
1,
|
|
991
|
+
expect.objectContaining({ toolName: "checkout_submit", confirmRequired: true }),
|
|
992
|
+
);
|
|
993
|
+
expect(onConfirm.mock.calls[1]?.[0]).not.toHaveProperty("confirmRequired");
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it("reads server patterns at call time and matches the bare tool name", async () => {
|
|
997
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
998
|
+
registry.tools = [fakeTool({ name: "delete_account" }), fakeTool({ name: "deleted_items" })];
|
|
999
|
+
let server: string[] | undefined;
|
|
1000
|
+
const bridge = runtimeBridge({ enabled: true, onConfirm }, () => server);
|
|
1001
|
+
await bridge.executeToolCall("webmcp:delete_account", {});
|
|
1002
|
+
expect(onConfirm.mock.calls[0]?.[0]).not.toHaveProperty("confirmRequired");
|
|
1003
|
+
|
|
1004
|
+
// Init lands after the bridge was built.
|
|
1005
|
+
server = ["delete_account", "*_admin"];
|
|
1006
|
+
await bridge.executeToolCall("webmcp:delete_account", {});
|
|
1007
|
+
await bridge.executeToolCall("webmcp:deleted_items", {});
|
|
1008
|
+
expect(onConfirm.mock.calls[1]?.[0]).toMatchObject({ confirmRequired: true });
|
|
1009
|
+
expect(onConfirm.mock.calls[2]?.[0]).not.toHaveProperty("confirmRequired");
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it("merges widget and server patterns", async () => {
|
|
1013
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
1014
|
+
registry.tools = [fakeTool({ name: "pay_now" }), fakeTool({ name: "wipe_cart" })];
|
|
1015
|
+
const bridge = runtimeBridge(
|
|
1016
|
+
{ enabled: true, onConfirm, requireConfirmFor: ["pay_*"] },
|
|
1017
|
+
() => ["wipe_*"],
|
|
1018
|
+
);
|
|
1019
|
+
await bridge.executeToolCall("webmcp:pay_now", {});
|
|
1020
|
+
await bridge.executeToolCall("webmcp:wipe_cart", {});
|
|
1021
|
+
expect(onConfirm.mock.calls.map((c) => c[0]?.confirmRequired)).toEqual([true, true]);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it.each([[undefined], [[] as string[]]])(
|
|
1025
|
+
"an absent or empty list leaves the gate unflagged (%s)",
|
|
1026
|
+
async (list) => {
|
|
1027
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => true);
|
|
1028
|
+
registry.tools = [fakeTool({ name: "checkout_submit" })];
|
|
1029
|
+
const bridge = runtimeBridge({ enabled: true, onConfirm, requireConfirmFor: list }, () => list);
|
|
1030
|
+
await bridge.executeToolCall("webmcp:checkout_submit", {});
|
|
1031
|
+
expect(onConfirm.mock.calls[0]?.[0]).not.toHaveProperty("confirmRequired");
|
|
1032
|
+
},
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
it("fails closed while the server policy is not loaded (null) or its read throws", async () => {
|
|
1036
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => false);
|
|
1037
|
+
const execute = vi.fn(() => "ran");
|
|
1038
|
+
registry.tools = [fakeTool({ name: "next_slide", annotations: { readOnlyHint: true }, execute })];
|
|
1039
|
+
const unloaded = runtimeBridge({ enabled: true, onConfirm }, () => null);
|
|
1040
|
+
const throwing = runtimeBridge({ enabled: true, onConfirm }, () => {
|
|
1041
|
+
throw new Error("boom");
|
|
1042
|
+
});
|
|
1043
|
+
for (const bridge of [unloaded, throwing]) {
|
|
1044
|
+
const r = await bridge.executeVoiceToolCall("webmcp:next_slide", {}, { autoConfirmReadOnly: true });
|
|
1045
|
+
expect(r.status).toBe("denied");
|
|
1046
|
+
}
|
|
1047
|
+
expect(onConfirm).toHaveBeenCalledTimes(2);
|
|
1048
|
+
expect(onConfirm.mock.calls.map((c) => c[0]?.confirmRequired)).toEqual([true, true]);
|
|
1049
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
it("does not let the voice read-only fast tier skip a matched tool", async () => {
|
|
1053
|
+
const onConfirm = vi.fn<WebMcpConfirmHandler>(async () => false);
|
|
1054
|
+
const execute = vi.fn(() => "ran");
|
|
1055
|
+
registry.tools = [
|
|
1056
|
+
fakeTool({ name: "checkout_preview", annotations: { readOnlyHint: true }, execute }),
|
|
1057
|
+
];
|
|
1058
|
+
const bridge = runtimeBridge({ enabled: true, onConfirm }, () => ["checkout_*"]);
|
|
1059
|
+
const r = await bridge.executeVoiceToolCall("webmcp:checkout_preview", {}, {
|
|
1060
|
+
autoConfirmReadOnly: true,
|
|
1061
|
+
});
|
|
1062
|
+
expect(onConfirm).toHaveBeenCalledWith(expect.objectContaining({ confirmRequired: true }));
|
|
1063
|
+
expect(execute).not.toHaveBeenCalled();
|
|
1064
|
+
expect(r.status).toBe("denied");
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
@@ -35,6 +35,14 @@ export interface WebMcpBridgeDeps {
|
|
|
35
35
|
getToolDisplayTitle: (toolName: string) => string | undefined;
|
|
36
36
|
/** Core's `loadWebMcpPolyfillModule` (slot-aware polyfill resolution). */
|
|
37
37
|
loadPolyfill: () => Promise<WebMcpPolyfillModule>;
|
|
38
|
+
/**
|
|
39
|
+
* Server-supplied `requireConfirmFor` patterns (the bound surface's policy
|
|
40
|
+
* from client-token init), read at call time because init can land after
|
|
41
|
+
* the bridge is built. Merged with `config.requireConfirmFor`. `null` means
|
|
42
|
+
* a policy is expected but not loaded yet: every call then needs
|
|
43
|
+
* confirmation (fail closed).
|
|
44
|
+
*/
|
|
45
|
+
getRequireConfirmFor?: () => readonly string[] | null | undefined;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
const DEFAULT_TOOL_TIMEOUT_MS = 30_000;
|
|
@@ -339,7 +347,13 @@ export class WebMcpBridge {
|
|
|
339
347
|
...(signal ? { signal } : {}),
|
|
340
348
|
reason: "gate",
|
|
341
349
|
};
|
|
342
|
-
|
|
350
|
+
// A `requireConfirmFor` match always reaches the confirm handler: it skips
|
|
351
|
+
// `autoApprove` on the default UI (session.ts) and the voice read-only
|
|
352
|
+
// fast tier here.
|
|
353
|
+
const confirmRequired = this.#requiresConfirmation(bareName);
|
|
354
|
+
if (confirmRequired) gateInfo.confirmRequired = true;
|
|
355
|
+
const skipGate =
|
|
356
|
+
!confirmRequired && autoConfirmReadOnly && annotations?.readOnlyHint === true;
|
|
343
357
|
if (!skipGate && !(await this.#requestConfirm(gateInfo))) {
|
|
344
358
|
return runFailure("denied", "User declined the tool call.");
|
|
345
359
|
}
|
|
@@ -487,6 +501,20 @@ export class WebMcpBridge {
|
|
|
487
501
|
}
|
|
488
502
|
}
|
|
489
503
|
|
|
504
|
+
#requiresConfirmation(toolName: string): boolean {
|
|
505
|
+
let serverPatterns: readonly string[] | null | undefined;
|
|
506
|
+
try {
|
|
507
|
+
serverPatterns = this.#deps.getRequireConfirmFor?.();
|
|
508
|
+
} catch {
|
|
509
|
+
serverPatterns = null;
|
|
510
|
+
}
|
|
511
|
+
if (serverPatterns === null) return true;
|
|
512
|
+
const matches = (list: readonly string[] | undefined): boolean =>
|
|
513
|
+
Array.isArray(list) &&
|
|
514
|
+
list.some((pattern) => typeof pattern === "string" && matchesGlob(toolName, pattern));
|
|
515
|
+
return matches(this.config.requireConfirmFor) || matches(serverPatterns);
|
|
516
|
+
}
|
|
517
|
+
|
|
490
518
|
#passesClientAllowlist(toolName: string): boolean {
|
|
491
519
|
const list = this.config.allowlist;
|
|
492
520
|
if (!list || list.length === 0) return true;
|
package/dist/chunk-BAkaTZpd.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const e=`data-persona-tooltip-suppressed`,t=`persona-composer-control`,n=`persona-composer-control--glyph`,r=(e,t,n)=>{let r=e.ownerDocument?.defaultView;if(!r)return;let i=r.getComputedStyle(e);for(let e of n){let n=i.getPropertyValue(e).trim();n?t.style.setProperty(e,n):t.style.removeProperty(e)}};export{e as i,n,r,t};
|