@noodleseed/assistant 1.23.0 → 1.25.0

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 (50) hide show
  1. package/README.md +4 -9
  2. package/dist/{app-view-element-7ZQZLYbL.d.ts → app-view-element-9ojWWYov.d.cts} +3 -2
  3. package/dist/{app-view-element-1i_w6FDf.d.cts → app-view-element-BM4lral-.d.ts} +3 -2
  4. package/dist/app-view.cjs +213 -41
  5. package/dist/app-view.cjs.map +1 -1
  6. package/dist/app-view.d.cts +3 -3
  7. package/dist/app-view.d.ts +3 -3
  8. package/dist/app-view.js +2 -2
  9. package/dist/{appearance-Cc-eqSbp.d.cts → appearance-B6vSg8Ub.d.cts} +5 -0
  10. package/dist/{appearance-Cc-eqSbp.d.ts → appearance-B6vSg8Ub.d.ts} +5 -0
  11. package/dist/{chunk-IBDP6PXQ.js → chunk-7BQHW37I.js} +71 -3
  12. package/dist/chunk-7BQHW37I.js.map +1 -0
  13. package/dist/{chunk-5NBHHNI3.js → chunk-LNBUYHL7.js} +2 -2
  14. package/dist/chunk-LNBUYHL7.js.map +1 -0
  15. package/dist/{chunk-XKTUXFGX.js → chunk-P32MAR4K.js} +214 -42
  16. package/dist/chunk-P32MAR4K.js.map +1 -0
  17. package/dist/{chunk-X7UQDIGS.js → chunk-SGMECNNU.js} +94 -43
  18. package/dist/chunk-SGMECNNU.js.map +1 -0
  19. package/dist/{client-Bp_VKpMg.d.ts → client-BkogON2A.d.ts} +11 -1
  20. package/dist/{client-uo57Swlm.d.cts → client-C07ppTU_.d.cts} +11 -1
  21. package/dist/client.cjs +70 -2
  22. package/dist/client.cjs.map +1 -1
  23. package/dist/client.d.cts +2 -2
  24. package/dist/client.d.ts +2 -2
  25. package/dist/client.js +1 -1
  26. package/dist/embed.global.js +149 -114
  27. package/dist/index.cjs +396 -105
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +3 -3
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.js +3 -3
  32. package/dist/react/client.cjs +70 -2
  33. package/dist/react/client.cjs.map +1 -1
  34. package/dist/react/client.d.cts +2 -2
  35. package/dist/react/client.d.ts +2 -2
  36. package/dist/react/client.js +1 -1
  37. package/dist/react.cjs +396 -105
  38. package/dist/react.cjs.map +1 -1
  39. package/dist/react.d.cts +3 -3
  40. package/dist/react.d.ts +3 -3
  41. package/dist/react.js +4 -4
  42. package/dist/server.cjs.map +1 -1
  43. package/dist/server.d.cts +3 -1
  44. package/dist/server.d.ts +3 -1
  45. package/dist/server.js.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/chunk-5NBHHNI3.js.map +0 -1
  48. package/dist/chunk-IBDP6PXQ.js.map +0 -1
  49. package/dist/chunk-X7UQDIGS.js.map +0 -1
  50. package/dist/chunk-XKTUXFGX.js.map +0 -1
package/README.md CHANGED
@@ -35,8 +35,8 @@ troubleshooting) lives at <https://docs.noodleseed.dev/guides/embedded-assistant
35
35
  the server itself is `@noodleseed/one` (`npm install -g @noodleseed/one`).
36
36
 
37
37
  If your page sends a `Content-Security-Policy`, allow the Noodle service origin in `connect-src` (turns and
38
- event streams) **and** `frame-src` (app widgets render inside a hosted sandbox document served from the
39
- service origin, `endpoints.sandbox`, so your `script-src` can stay strict). Details are in the guide's
38
+ event streams) and the exact session-advertised `endpoints.sandbox` origin in `frame-src` (it may be a
39
+ dedicated sandbox origin, so your `script-src` can stay strict). Details are in the guide's
40
40
  "Content-Security-Policy on your page" section.
41
41
 
42
42
  ## Quick start
@@ -410,13 +410,8 @@ import "@noodleseed/assistant/app-view";
410
410
  </template>
411
411
  ```
412
412
 
413
- Configure Vue's `isCustomElement` for `noodle-app-view`. Angular uses the same element with `[client]`,
414
- `[view]`, and `[theme]` property bindings. Do not serialize `client` or `view` into attributes.
415
-
416
- `<noodle-app-view>` is the canonical host; `NoodleAppView` delegates to it. Both use the
417
- service-advertised sandbox URL, route App calls through the supplied client, publish theme changes without
418
- replacing the iframe, and request standard teardown on semantic replacement, disconnect, or an App teardown
419
- request.
413
+ Framework setup, lifecycle ownership, sandbox/CSP behavior, and staged diagnostics are owned by the
414
+ [Bring your own UI guide](https://docs.noodleseed.dev/docs/guides/bring-your-own-ui).
420
415
 
421
416
  The App document owns its action intent: it calls standard `tools/call` after connecting and never relies on
422
417
  native form navigation. If that call needs input or confirmation, the same `client` publishes the normal
@@ -1,9 +1,10 @@
1
- import { A as AssistantClient, a as AssistantViewAvailableDetail, e as AssistantErrorDetail } from './client-Bp_VKpMg.js';
1
+ import { A as AssistantClient, a as AssistantViewAvailableDetail, e as AssistantErrorDetail } from './client-C07ppTU_.cjs';
2
2
 
3
3
  declare const APP_VIEW_TAG_NAME = "noodle-app-view";
4
4
  interface AssistantAppViewErrorDetail {
5
- readonly code: 'view_render_timeout';
5
+ readonly code: 'view_render_timeout' | 'view_bridge_error';
6
6
  readonly retryable: false;
7
+ readonly stage: 'sandbox_load' | 'sandbox_proxy' | 'app_initialize';
7
8
  }
8
9
  declare global {
9
10
  interface HTMLElementTagNameMap {
@@ -1,9 +1,10 @@
1
- import { A as AssistantClient, a as AssistantViewAvailableDetail, e as AssistantErrorDetail } from './client-uo57Swlm.cjs';
1
+ import { A as AssistantClient, a as AssistantViewAvailableDetail, e as AssistantErrorDetail } from './client-BkogON2A.js';
2
2
 
3
3
  declare const APP_VIEW_TAG_NAME = "noodle-app-view";
4
4
  interface AssistantAppViewErrorDetail {
5
- readonly code: 'view_render_timeout';
5
+ readonly code: 'view_render_timeout' | 'view_bridge_error';
6
6
  readonly retryable: false;
7
+ readonly stage: 'sandbox_load' | 'sandbox_proxy' | 'app_initialize';
7
8
  }
8
9
  declare global {
9
10
  interface HTMLElementTagNameMap {
package/dist/app-view.cjs CHANGED
@@ -15197,7 +15197,7 @@ __export(app_view_exports, {
15197
15197
  });
15198
15198
  module.exports = __toCommonJS(app_view_exports);
15199
15199
 
15200
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
15200
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
15201
15201
  init_v4();
15202
15202
  var RELATED_TASK_META_KEY = "io.modelcontextprotocol/related-task";
15203
15203
  var JSONRPC_VERSION = "2.0";
@@ -16718,7 +16718,7 @@ var UrlElicitationRequiredError = class extends McpError {
16718
16718
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/mini/parse.js
16719
16719
  init_core2();
16720
16720
 
16721
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
16721
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
16722
16722
  function isZ4Schema(s2) {
16723
16723
  const schema = s2;
16724
16724
  return !!schema._zod;
@@ -16781,7 +16781,7 @@ function getLiteralValue(schema) {
16781
16781
  return void 0;
16782
16782
  }
16783
16783
 
16784
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
16784
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
16785
16785
  function isTerminal(status) {
16786
16786
  return status === "completed" || status === "failed" || status === "cancelled";
16787
16787
  }
@@ -16789,7 +16789,7 @@ function isTerminal(status) {
16789
16789
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@4.4.3/node_modules/zod-to-json-schema/dist/esm/parsers/string.js
16790
16790
  var ALPHA_NUMERIC = new Set("ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvxyz0123456789");
16791
16791
 
16792
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
16792
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
16793
16793
  function getMethodLiteral(schema) {
16794
16794
  const shape = getObjectShape(schema);
16795
16795
  const methodSchema = shape?.method;
@@ -16810,7 +16810,7 @@ function parseWithCompat(schema, data) {
16810
16810
  return result.data;
16811
16811
  }
16812
16812
 
16813
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
16813
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_supports-color@7.2.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
16814
16814
  var DEFAULT_REQUEST_TIMEOUT_MSEC = 6e4;
16815
16815
  var Protocol = class {
16816
16816
  constructor(_options) {
@@ -17922,6 +17922,15 @@ var E = class {
17922
17922
  sessionId;
17923
17923
  setProtocolVersion;
17924
17924
  };
17925
+ function AZ(X) {
17926
+ if (!X) return "";
17927
+ let Y = [];
17928
+ if (X.camera) Y.push("camera");
17929
+ if (X.microphone) Y.push("microphone");
17930
+ if (X.geolocation) Y.push("geolocation");
17931
+ if (X.clipboardWrite) Y.push("clipboard-write");
17932
+ return Y.join("; ");
17933
+ }
17925
17934
  var KX = [j];
17926
17935
  var DX = class extends _ {
17927
17936
  _client;
@@ -18192,34 +18201,131 @@ function GX(X, Y) {
18192
18201
  return JSON.stringify(X) === JSON.stringify(Y);
18193
18202
  }
18194
18203
 
18204
+ // src/app-sandbox-policy.ts
18205
+ var MAX_DOMAINS_PER_DIRECTIVE = 32;
18206
+ function isRecord(value) {
18207
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18208
+ }
18209
+ function isLoopback(hostname3) {
18210
+ return hostname3 === "localhost" || hostname3 === "127.0.0.1" || hostname3 === "[::1]";
18211
+ }
18212
+ function normalizeDomain(value, connect) {
18213
+ if (value.length > 512 || /[;\r\n'"\s]/u.test(value)) return void 0;
18214
+ try {
18215
+ const parsed = new URL(value);
18216
+ const secure = parsed.protocol === "https:" || connect && parsed.protocol === "wss:";
18217
+ const local = isLoopback(parsed.hostname) && (parsed.protocol === "http:" || connect && parsed.protocol === "ws:");
18218
+ if (!secure && !local) return void 0;
18219
+ if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
18220
+ return void 0;
18221
+ }
18222
+ return parsed.origin;
18223
+ } catch {
18224
+ return void 0;
18225
+ }
18226
+ }
18227
+ function normalizeDomains(values, connect) {
18228
+ const normalized = [
18229
+ ...new Set(
18230
+ values.slice(0, MAX_DOMAINS_PER_DIRECTIVE).map((value) => typeof value === "string" ? normalizeDomain(value, connect) : void 0).filter((value) => value !== void 0)
18231
+ )
18232
+ ];
18233
+ return normalized;
18234
+ }
18235
+ function resolveAssistantAppSandboxPolicy(resourceMeta) {
18236
+ if (!isRecord(resourceMeta)) return {};
18237
+ const rawCsp = resourceMeta.csp;
18238
+ const csp = isRecord(rawCsp) ? {
18239
+ ...Array.isArray(rawCsp.connectDomains) ? { connectDomains: normalizeDomains(rawCsp.connectDomains, true) } : {},
18240
+ ...Array.isArray(rawCsp.resourceDomains) ? { resourceDomains: normalizeDomains(rawCsp.resourceDomains, false) } : {},
18241
+ ...Array.isArray(rawCsp.frameDomains) ? { frameDomains: normalizeDomains(rawCsp.frameDomains, false) } : {},
18242
+ ...Array.isArray(rawCsp.baseUriDomains) ? { baseUriDomains: normalizeDomains(rawCsp.baseUriDomains, false) } : {}
18243
+ } : void 0;
18244
+ const rawPermissions = resourceMeta.permissions;
18245
+ const permissions = isRecord(rawPermissions) ? {
18246
+ ...isRecord(rawPermissions.camera) ? { camera: {} } : {},
18247
+ ...isRecord(rawPermissions.microphone) ? { microphone: {} } : {},
18248
+ ...isRecord(rawPermissions.geolocation) ? { geolocation: {} } : {},
18249
+ ...isRecord(rawPermissions.clipboardWrite) ? { clipboardWrite: {} } : {}
18250
+ } : void 0;
18251
+ const allowAttribute = AZ(permissions);
18252
+ return {
18253
+ ...csp === void 0 ? {} : { csp },
18254
+ ...permissions === void 0 ? {} : { permissions },
18255
+ ...allowAttribute ? { allowAttribute } : {}
18256
+ };
18257
+ }
18258
+ function assistantAppSandboxUrl(sandboxUrl, policy) {
18259
+ const parsed = new URL(sandboxUrl);
18260
+ if (policy.csp) parsed.searchParams.set("csp", JSON.stringify(policy.csp));
18261
+ const parentOrigin = globalThis.location?.origin;
18262
+ if (parentOrigin && parentOrigin !== "null") {
18263
+ parsed.hash = new URLSearchParams({ parentOrigin }).toString();
18264
+ }
18265
+ return parsed.href;
18266
+ }
18267
+
18195
18268
  // src/app-host.ts
18196
- var PROXY_DOCUMENT = `<!doctype html><meta charset="utf-8"><style>html,body,iframe{border:0;margin:0;width:100%;height:100%;overflow:hidden}body{background:transparent}</style><script>
18197
- let inner;
18269
+ var PROXY_DOCUMENT = `<!doctype html><meta charset="utf-8"><style>html,body,iframe{border:0;margin:0;width:100%;height:100%;overflow:hidden}body{background:transparent}</style><body><script>
18270
+ let inner,loaded=false;
18271
+ if(self===top)throw new Error('The assistant sandbox relay must be embedded');
18272
+ const standardSandbox='allow-scripts allow-same-origin allow-forms';
18273
+ const rawParentOrigin=new URLSearchParams(location.hash.slice(1)).get('parentOrigin');
18274
+ let expectedParentOrigin;
18275
+ try{const parsed=new URL(rawParentOrigin||'');if(parsed.origin===rawParentOrigin&&/^https?:$/.test(parsed.protocol))expectedParentOrigin=parsed.origin}catch{}
18276
+ const parentTarget=expectedParentOrigin||'*';
18277
+ const ownOrigin=self.origin;
18198
18278
  const ready={jsonrpc:'2.0',method:'ui/notifications/sandbox-proxy-ready',params:{}};
18199
18279
  // Re-announce until the host acknowledges with the resource: when this document is hosted
18200
18280
  // cross-origin, the first announcement can arrive before the host's load handler is listening.
18201
- const announce=setInterval(()=>parent.postMessage(ready,'*'),120);
18281
+ const announce=setInterval(()=>parent.postMessage(ready,parentTarget),120);
18202
18282
  setTimeout(()=>clearInterval(announce),15000);
18203
18283
  addEventListener('message',(event)=>{
18204
18284
  if(event.source===parent){
18285
+ if(expectedParentOrigin&&event.origin!==expectedParentOrigin)return;
18205
18286
  const message=event.data;
18206
18287
  if(message?.method==='ui/notifications/sandbox-resource-ready'){
18207
18288
  clearInterval(announce);
18208
- if(inner)return;
18289
+ if(loaded)return;
18290
+ loaded=true;
18291
+ const requestedSandbox=message.params?.sandbox;
18209
18292
  inner=document.createElement('iframe');
18210
- inner.setAttribute('sandbox',message.params?.sandbox||'allow-scripts');
18293
+ inner.setAttribute('sandbox',requestedSandbox===standardSandbox?standardSandbox:'allow-scripts');
18211
18294
  inner.setAttribute('referrerpolicy','no-referrer');
18212
- inner.srcdoc=String(message.params?.html||'');
18213
- document.body.replaceChildren(inner);
18295
+ const permissions=message.params?.permissions;
18296
+ const allow=[];
18297
+ if(permissions?.camera)allow.push('camera');
18298
+ if(permissions?.microphone)allow.push('microphone');
18299
+ if(permissions?.geolocation)allow.push('geolocation');
18300
+ if(permissions?.clipboardWrite)allow.push('clipboard-write');
18301
+ if(allow.length)inner.setAttribute('allow',allow.join('; '));
18302
+ document.body.append(inner);
18303
+ let html=String(message.params?.html||'');
18304
+ const csp=message.params?.csp;
18305
+ if(csp&&typeof csp==='object'){
18306
+ const list=(value,fallback)=>Array.isArray(value)&&value.length?value.join(' '):fallback;
18307
+ const resources=Array.isArray(csp.resourceDomains)?csp.resourceDomains:[];
18308
+ const suffix=resources.length?' '+resources.join(' '):'';
18309
+ const policy=["default-src 'none'","script-src 'unsafe-inline'"+suffix,"style-src 'unsafe-inline'"+suffix,'img-src data: blob:'+suffix,'font-src data: blob:'+suffix,'media-src data: blob:'+suffix,'worker-src blob:'+suffix,'connect-src '+list(csp.connectDomains,"'none'"),'frame-src '+list(csp.frameDomains,"'none'"),"object-src 'none'","form-action 'none'",'base-uri '+list(csp.baseUriDomains,"'none'")].join('; ');
18310
+ const meta='<meta http-equiv="Content-Security-Policy" content="'+policy.replace(/&/g,'&amp;').replace(/"/g,'&quot;')+'">';
18311
+ const doctype=html.match(/^\\s*<!doctype[^>]*>/i);
18312
+ html=doctype?html.slice(0,doctype[0].length)+meta+html.slice(doctype[0].length):meta+html;
18313
+ }
18314
+ try{
18315
+ const doc=inner.contentDocument||inner.contentWindow?.document;
18316
+ if(!doc)throw new Error('document unavailable');
18317
+ doc.open();doc.write(html);doc.close();
18318
+ }catch{inner.srcdoc=html}
18214
18319
  return;
18215
18320
  }
18216
18321
  inner?.contentWindow?.postMessage(message,'*');
18217
18322
  } else if(inner && event.source===inner.contentWindow) {
18218
- parent.postMessage(event.data,'*');
18323
+ if(event.origin!==ownOrigin)return;
18324
+ parent.postMessage(event.data,parentTarget);
18219
18325
  }
18220
18326
  });
18221
- parent.postMessage(ready,'*');
18222
- </script>`;
18327
+ parent.postMessage(ready,parentTarget);
18328
+ </script></body>`;
18223
18329
  function isAllowedAppLink(value, allowedDomains, baseUrl) {
18224
18330
  try {
18225
18331
  const parsed = new URL(value, baseUrl);
@@ -18246,16 +18352,42 @@ function createAssistantAppHostContext(theme, allowFullscreen = false, displayMo
18246
18352
  }
18247
18353
  var APP_RENDER_TIMEOUT_MS = 1e4;
18248
18354
  var APP_TEARDOWN_TIMEOUT_MS = 1e3;
18355
+ var STANDARD_SANDBOX = "allow-scripts allow-same-origin allow-forms";
18249
18356
  var mountedApps = /* @__PURE__ */ new WeakMap();
18250
18357
  function resolveSandboxUrl(value) {
18251
18358
  if (!value) return void 0;
18252
18359
  try {
18253
18360
  const parsed = new URL(value);
18254
- return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.href : void 0;
18361
+ const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
18362
+ const permittedProtocol = parsed.protocol === "https:" || parsed.protocol === "http:" && loopback;
18363
+ const embedderOrigin = globalThis.location?.origin;
18364
+ if (!permittedProtocol || embedderOrigin && embedderOrigin !== "null" && parsed.origin === embedderOrigin) {
18365
+ return void 0;
18366
+ }
18367
+ return parsed.href;
18255
18368
  } catch {
18256
18369
  return void 0;
18257
18370
  }
18258
18371
  }
18372
+ function copyAppJson(value, depth = 0) {
18373
+ if (depth > 64) throw new Error("App payload nesting exceeds the host limit");
18374
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
18375
+ if (typeof value === "number") {
18376
+ if (!Number.isFinite(value)) throw new Error("App payload numbers must be finite");
18377
+ return value;
18378
+ }
18379
+ if (Array.isArray(value)) return value.map((entry) => copyAppJson(entry, depth + 1));
18380
+ const copied = {};
18381
+ for (const [key, entry] of Object.entries(value)) {
18382
+ Object.defineProperty(copied, key, {
18383
+ value: copyAppJson(entry, depth + 1),
18384
+ enumerable: true,
18385
+ configurable: true,
18386
+ writable: true
18387
+ });
18388
+ }
18389
+ return copied;
18390
+ }
18259
18391
  function mountAssistantApp(detail, actions, options = {}) {
18260
18392
  if (!detail.html) return void 0;
18261
18393
  const card = document.createElement("section");
@@ -18276,16 +18408,25 @@ function mountAssistantApp(detail, actions, options = {}) {
18276
18408
  const frame = document.createElement("iframe");
18277
18409
  frame.className = "noodle-app-frame";
18278
18410
  frame.title = detail.title ?? detail.tool;
18279
- frame.setAttribute("sandbox", "allow-scripts");
18411
+ const policy = resolveAssistantAppSandboxPolicy(detail.resourceMeta);
18280
18412
  frame.setAttribute("referrerpolicy", "no-referrer");
18281
18413
  const sandboxUrl = resolveSandboxUrl(options.sandboxUrl);
18282
- if (sandboxUrl) frame.src = sandboxUrl;
18283
- else frame.srcdoc = PROXY_DOCUMENT;
18414
+ const appliedPolicy = sandboxUrl ? policy : { csp: policy.csp ?? {} };
18415
+ if (sandboxUrl) {
18416
+ frame.setAttribute("sandbox", STANDARD_SANDBOX);
18417
+ if (policy.allowAttribute) frame.setAttribute("allow", policy.allowAttribute);
18418
+ frame.src = assistantAppSandboxUrl(sandboxUrl, policy);
18419
+ } else {
18420
+ frame.setAttribute("sandbox", "allow-scripts");
18421
+ frame.srcdoc = PROXY_DOCUMENT;
18422
+ }
18284
18423
  card.append(frame);
18285
18424
  let mountedBridge;
18286
18425
  let mountedTransport;
18287
18426
  let destroyed = false;
18288
18427
  let initialized = false;
18428
+ let renderFailed = false;
18429
+ let renderStage = "sandbox_load";
18289
18430
  let currentTheme = actions.theme;
18290
18431
  let displayMode = "inline";
18291
18432
  let hostContext = createAssistantAppHostContext(
@@ -18294,6 +18435,15 @@ function mountAssistantApp(detail, actions, options = {}) {
18294
18435
  displayMode
18295
18436
  );
18296
18437
  let destroyPromise;
18438
+ let transportClosePromise;
18439
+ let renderTimer;
18440
+ const closeTransport = () => {
18441
+ if (transportClosePromise) return transportClosePromise;
18442
+ const closing = mountedTransport?.close().catch(() => {
18443
+ }) ?? Promise.resolve();
18444
+ transportClosePromise = closing;
18445
+ return closing;
18446
+ };
18297
18447
  const applyDisplayMode = (requestedMode) => {
18298
18448
  displayMode = requestedMode === "fullscreen" && options.allowFullscreen === true ? "fullscreen" : "inline";
18299
18449
  card.toggleAttribute("data-fullscreen", displayMode === "fullscreen");
@@ -18311,18 +18461,26 @@ function mountAssistantApp(detail, actions, options = {}) {
18311
18461
  applyDisplayMode("inline");
18312
18462
  frame.focus();
18313
18463
  });
18314
- const renderTimer = setTimeout(() => {
18315
- if (initialized || !frame.isConnected) return;
18464
+ const showRenderFailure = (code) => {
18465
+ if (code === "view_render_timeout" && initialized || destroyed || renderFailed || !frame.isConnected)
18466
+ return;
18467
+ renderFailed = true;
18468
+ clearTimeout(renderTimer);
18316
18469
  const note = document.createElement("div");
18317
18470
  note.className = "noodle-app-fallback";
18318
18471
  note.textContent = "This app view could not be displayed.";
18319
18472
  frame.replaceWith(note);
18320
- options.onRenderFailure?.({ code: "view_render_timeout", retryable: false });
18473
+ options.onRenderFailure?.({ code, retryable: false, stage: renderStage });
18474
+ void closeTransport();
18475
+ };
18476
+ renderTimer = setTimeout(() => {
18477
+ showRenderFailure("view_render_timeout");
18321
18478
  }, APP_RENDER_TIMEOUT_MS);
18322
18479
  frame.addEventListener(
18323
18480
  "load",
18324
18481
  () => {
18325
- if (destroyed) return;
18482
+ if (destroyed || renderFailed) return;
18483
+ renderStage = "sandbox_proxy";
18326
18484
  const target = frame.contentWindow;
18327
18485
  if (!target) return;
18328
18486
  const bridge = new DX(
@@ -18333,7 +18491,11 @@ function mountAssistantApp(detail, actions, options = {}) {
18333
18491
  serverTools: {},
18334
18492
  serverResources: {},
18335
18493
  updateModelContext: { text: {}, structuredContent: {} },
18336
- message: { text: {} }
18494
+ message: { text: {} },
18495
+ sandbox: {
18496
+ ...appliedPolicy.csp === void 0 ? {} : { csp: appliedPolicy.csp },
18497
+ ...appliedPolicy.permissions === void 0 ? {} : { permissions: appliedPolicy.permissions }
18498
+ }
18337
18499
  },
18338
18500
  {
18339
18501
  hostContext
@@ -18341,13 +18503,15 @@ function mountAssistantApp(detail, actions, options = {}) {
18341
18503
  );
18342
18504
  mountedBridge = bridge;
18343
18505
  bridge.onsandboxready = () => {
18344
- if (destroyed) return;
18506
+ if (destroyed || renderFailed) return;
18507
+ renderStage = "app_initialize";
18345
18508
  const current = options.getDetail?.() ?? detail;
18346
18509
  void bridge.sendSandboxResourceReady({
18347
18510
  html: current.html,
18348
- sandbox: "allow-scripts",
18349
- ...current.resourceMeta ?? {}
18350
- });
18511
+ sandbox: sandboxUrl ? STANDARD_SANDBOX : "allow-scripts",
18512
+ ...appliedPolicy.csp === void 0 ? {} : { csp: appliedPolicy.csp },
18513
+ ...appliedPolicy.permissions === void 0 ? {} : { permissions: appliedPolicy.permissions }
18514
+ }).catch(() => showRenderFailure("view_bridge_error"));
18351
18515
  };
18352
18516
  bridge.onsizechange = ({ height }) => {
18353
18517
  if (destroyed) return;
@@ -18411,21 +18575,30 @@ function mountAssistantApp(detail, actions, options = {}) {
18411
18575
  return await actions.client.requestApp("resources/read", params);
18412
18576
  };
18413
18577
  bridge.oninitialized = () => {
18414
- if (destroyed) return;
18415
- initialized = true;
18416
- clearTimeout(renderTimer);
18417
- bridge.setHostContext(hostContext);
18418
- const current = options.getDetail?.() ?? detail;
18419
- void bridge.sendToolInput({ arguments: current.arguments ?? {} });
18420
- void bridge.sendToolResult({
18421
- content: [{ type: "text", text: JSON.stringify(current.result) }],
18422
- ...typeof current.result === "object" && current.result !== null && !Array.isArray(current.result) ? { structuredContent: current.result } : {}
18423
- });
18578
+ if (destroyed || renderFailed) return;
18579
+ try {
18580
+ const current = options.getDetail?.() ?? detail;
18581
+ const copiedArguments = copyAppJson(current.arguments ?? {});
18582
+ const copiedResult = copyAppJson(current.result);
18583
+ const argumentsRecord = typeof copiedArguments === "object" && copiedArguments !== null && !Array.isArray(copiedArguments) ? copiedArguments : {};
18584
+ initialized = true;
18585
+ clearTimeout(renderTimer);
18586
+ bridge.setHostContext(hostContext);
18587
+ void Promise.all([
18588
+ bridge.sendToolInput({ arguments: argumentsRecord }),
18589
+ bridge.sendToolResult({
18590
+ content: [{ type: "text", text: JSON.stringify(copiedResult) }],
18591
+ ...typeof copiedResult === "object" && copiedResult !== null && !Array.isArray(copiedResult) ? { structuredContent: copiedResult } : {}
18592
+ })
18593
+ ]).catch(() => showRenderFailure("view_bridge_error"));
18594
+ } catch {
18595
+ showRenderFailure("view_bridge_error");
18596
+ }
18424
18597
  };
18425
18598
  const transport = new E(target, target);
18426
18599
  mountedTransport = transport;
18427
18600
  void bridge.connect(transport).catch(() => {
18428
- void destroy();
18601
+ showRenderFailure("view_bridge_error");
18429
18602
  });
18430
18603
  },
18431
18604
  { once: true }
@@ -18451,8 +18624,7 @@ function mountAssistantApp(detail, actions, options = {}) {
18451
18624
  }
18452
18625
  } catch {
18453
18626
  }
18454
- await mountedTransport?.close().catch(() => {
18455
- });
18627
+ await closeTransport();
18456
18628
  card.remove();
18457
18629
  mountedApps.delete(card);
18458
18630
  })();