@agent-native/core 0.206.0-nightly-20261009153146 → 0.206.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 (47) hide show
  1. package/dist/action.d.ts +7 -1
  2. package/dist/client/agent-chat/index.d.ts +1 -1
  3. package/dist/client/agent-chat/index.js +1 -1
  4. package/dist/client/embed-auth.d.ts +2 -0
  5. package/dist/client/embed-auth.js +160 -12
  6. package/dist/client/host/index.d.ts +1 -1
  7. package/dist/client/host/index.js +1 -1
  8. package/dist/client/mcp-app-host.d.ts +1 -0
  9. package/dist/client/mcp-app-host.js +4 -1
  10. package/dist/collab/routes.d.ts +1 -1
  11. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  12. package/dist/mcp/build-server.d.ts +15 -0
  13. package/dist/mcp/build-server.js +307 -91
  14. package/dist/mcp/connect-route.js +1 -0
  15. package/dist/mcp/embed-app.js +291 -31
  16. package/dist/mcp/oauth-route.js +3 -0
  17. package/dist/mcp/oauth-store.d.ts +2 -1
  18. package/dist/mcp/oauth-store.js +5 -6
  19. package/dist/mcp/oauth-token.d.ts +5 -0
  20. package/dist/mcp/oauth-token.js +17 -0
  21. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  22. package/dist/mcp/server.d.ts +2 -2
  23. package/dist/mcp/server.js +2 -2
  24. package/dist/observability/routes.d.ts +5 -5
  25. package/dist/org/handlers.d.ts +1 -1
  26. package/dist/org/workspace-app-access-request.d.ts +8 -0
  27. package/dist/org/workspace-app-access-request.js +20 -0
  28. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  29. package/dist/resources/handlers.d.ts +1 -1
  30. package/dist/secrets/routes.d.ts +11 -11
  31. package/dist/server/action-routes.d.ts +2 -0
  32. package/dist/server/action-routes.js +187 -31
  33. package/dist/server/agent-chat/mcp-options.d.ts +3 -1
  34. package/dist/server/agent-chat-plugin.js +26 -1
  35. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  36. package/dist/server/auth.js +11 -20
  37. package/dist/server/embed-route.js +5 -3
  38. package/dist/server/embed-session.d.ts +30 -1
  39. package/dist/server/embed-session.js +376 -30
  40. package/dist/server/realtime-token.d.ts +1 -1
  41. package/dist/server/request-context.d.ts +2 -0
  42. package/dist/server/request-context.js +25 -1
  43. package/dist/shared/embed-auth.d.ts +74 -0
  44. package/dist/shared/embed-auth.js +379 -10
  45. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  46. package/dist/triggers/actions/manage-automation.d.ts +3 -3
  47. package/package.json +4 -4
@@ -3,6 +3,7 @@ import { MCP_APP_HOST_FILL_ATTRIBUTE, MCP_APP_PANE_FILL_MAX_HEIGHT, } from "../s
3
3
  const MCP_APP_IMPORT = "https://esm.sh/@modelcontextprotocol/ext-apps@1.7.5/app-with-deps";
4
4
  export const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = "$requestOrigin";
5
5
  const MCP_APP_WRAPPER_CHROME_HEIGHT = 44;
6
+ const MCP_APP_INLINE_LAUNCHER_HEIGHT = 56;
6
7
  export const DEFAULT_MCP_APP_SHELL_HEIGHT = 560;
7
8
  export const DEFAULT_MCP_APP_VIEWPORT_HEIGHT = DEFAULT_MCP_APP_SHELL_HEIGHT - MCP_APP_WRAPPER_CHROME_HEIGHT;
8
9
  function attr(value) {
@@ -29,12 +30,9 @@ export function embedApp(options = {}) {
29
30
  ...(options.description ? { description: options.description } : {}),
30
31
  html: (ctx) => {
31
32
  const remoteBridgeFallbackEnabled = ctx.catalogMode !== "directory";
32
- // A directory widget is an editor that lives in a host pane, never a
33
- // content-sized card, so it fills the pane even when the host will not
34
- // give it a height (see paneFillHeight).
35
33
  const fillsPane = ctx.catalogMode === "directory";
36
34
  return `<!doctype html>
37
- <html lang="en"${fillsPane ? ` ${MCP_APP_HOST_FILL_ATTRIBUTE}="1"` : ""}>
35
+ <html lang="en">
38
36
  <head>
39
37
  <meta charset="utf-8">
40
38
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -60,6 +58,10 @@ export function embedApp(options = {}) {
60
58
  .fallback-copy { max-width: 520px; color: color-mix(in srgb, CanvasText 64%, Canvas); font-size: 13px; line-height: 1.45; }
61
59
  .fallback-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; }
62
60
  .fallback-url { max-width: min(560px, 100%); overflow-wrap: anywhere; color: color-mix(in srgb, CanvasText 76%, Canvas); font-size: 12px; }
61
+ body[data-catalog-mode="directory"][data-widget-mode="inline"] .shell { display: flex; align-items: center; height: ${MCP_APP_INLINE_LAUNCHER_HEIGHT}px; min-height: ${MCP_APP_INLINE_LAUNCHER_HEIGHT}px; max-height: ${MCP_APP_INLINE_LAUNCHER_HEIGHT}px; overflow: hidden; }
62
+ body[data-catalog-mode="directory"][data-widget-mode="inline"] .bar { width: 100%; height: ${MCP_APP_INLINE_LAUNCHER_HEIGHT}px; min-height: ${MCP_APP_INLINE_LAUNCHER_HEIGHT}px; padding: 0 12px; border: 0; }
63
+ body[data-catalog-mode="directory"][data-widget-mode="inline"] [data-display] { display: none !important; }
64
+ body[data-catalog-mode="directory"][data-widget-mode="inline"] .stage { display: none; }
63
65
  html[${MCP_APP_HOST_FILL_ATTRIBUTE}], html[${MCP_APP_HOST_FILL_ATTRIBUTE}] body { height: 100%; }
64
66
  html[${MCP_APP_HOST_FILL_ATTRIBUTE}] .shell { display: flex; flex-direction: column; gap: 0; height: 100vh; height: 100dvh; min-height: 0; }
65
67
  html[${MCP_APP_HOST_FILL_ATTRIBUTE}] .bar { display: none; }
@@ -73,6 +75,7 @@ export function embedApp(options = {}) {
73
75
  data-open-label="${attr(openLabel)}"
74
76
  data-start-tool="${attr(ctx.startToolName ?? startToolName)}"
75
77
  data-catalog-mode="${attr(ctx.catalogMode)}"
78
+ data-widget-mode="${fillsPane ? "inline" : "content"}"
76
79
  data-embed-default="${embedByDefault ? "1" : "0"}"
77
80
  >
78
81
  <main class="shell">
@@ -125,14 +128,18 @@ export function embedApp(options = {}) {
125
128
  let toolResponseMetadata = {};
126
129
  let openUrl = "";
127
130
  let openStartUrl = "";
131
+ let activeEmbedSessionTicket = "";
128
132
  let startedFor = "";
129
133
  let appFrame = null;
130
134
  let appFrameReady = false;
131
135
  let appFrameReadyTimer = null;
132
136
  let appFrameLoadTimer = null;
133
137
  let lastFrameSrc = "";
138
+ const appFrameDocumentState = { loadGeneration: 0 };
134
139
  let embedSessionRefreshAttempts = 0;
135
140
  let reportedContentHeight = 0;
141
+ let directoryWidgetOpenRequested = false;
142
+ let lastHostDisplayMode = "";
136
143
 
137
144
  function esc(value) {
138
145
  return String(value ?? "")
@@ -178,6 +185,27 @@ export function embedApp(options = {}) {
178
185
  return record.displayMode === "fullscreen" || record.displayMode === "pip";
179
186
  }
180
187
 
188
+ function isCompactDirectoryWidget() {
189
+ return fillsPane && body.dataset.widgetMode === "inline";
190
+ }
191
+
192
+ function updateDirectoryWidgetLayout() {
193
+ if (!fillsPane) return false;
194
+ const context = hostState().context || {};
195
+ const displayMode = context.displayMode;
196
+ if (displayMode === "inline" && lastHostDisplayMode === "fullscreen") {
197
+ directoryWidgetOpenRequested = false;
198
+ }
199
+ if (typeof displayMode === "string") {
200
+ lastHostDisplayMode = displayMode;
201
+ }
202
+ const inline =
203
+ !directoryWidgetOpenRequested &&
204
+ (displayMode === undefined || displayMode === "inline");
205
+ body.dataset.widgetMode = inline ? "inline" : "pane";
206
+ return inline;
207
+ }
208
+
181
209
  // A fullscreen view or fixed-height container sizes the frame itself, so
182
210
  // the shell fills it with CSS and never reports a height. An inline card is
183
211
  // the opposite: the host follows the height reported here, so filling the
@@ -187,15 +215,19 @@ export function embedApp(options = {}) {
187
215
  function applyHostFillMode() {
188
216
  const context = hostState().context || {};
189
217
  const hostFill = hostFillsContainer(context);
218
+ const compactInline = isCompactDirectoryWidget();
190
219
  const root = document.documentElement;
191
- if (hostFill || fillsPane) {
220
+ if (compactInline) {
221
+ root.removeAttribute(hostFillAttribute);
222
+ if (appFrame) appFrame.style.height = "";
223
+ } else if (hostFill || fillsPane) {
192
224
  root.setAttribute(hostFillAttribute, "1");
193
225
  if (appFrame) appFrame.style.height = "";
194
226
  } else {
195
227
  root.removeAttribute(hostFillAttribute);
196
228
  }
197
229
  body.dataset.hostFill = hostFill ? "1" : "0";
198
- body.dataset.paneFill = fillsPane && !hostFill ? "1" : "0";
230
+ body.dataset.paneFill = fillsPane && !hostFill && !compactInline ? "1" : "0";
199
231
  body.dataset.hostDisplayMode = typeof context.displayMode === "string" ? context.displayMode : "";
200
232
  return hostFill;
201
233
  }
@@ -222,7 +254,13 @@ export function embedApp(options = {}) {
222
254
  // the frame's height; in a pane the shell owns it, so tell the app so.
223
255
  function hostStateForApp() {
224
256
  const state = hostState();
225
- if (!fillsPane || hostFillsContainer(state.context)) return state;
257
+ if (
258
+ !fillsPane ||
259
+ isCompactDirectoryWidget() ||
260
+ hostFillsContainer(state.context)
261
+ ) {
262
+ return state;
263
+ }
226
264
  const context = objectValue(state.context);
227
265
  return {
228
266
  ...state,
@@ -298,7 +336,11 @@ export function embedApp(options = {}) {
298
336
  function toolResultMeta(params) {
299
337
  if (!params || typeof params !== "object") return {};
300
338
  const direct = metadataRecord(params);
301
- if (direct) return direct;
339
+ if (direct) {
340
+ const mcpToolResult = direct.mcp_tool_result;
341
+ const mcpMeta = metadataRecord(mcpToolResult);
342
+ return mcpMeta || direct;
343
+ }
302
344
  if (params.result && typeof params.result === "object") {
303
345
  return toolResultMeta(params.result);
304
346
  }
@@ -396,8 +438,31 @@ export function embedApp(options = {}) {
396
438
  };
397
439
  }
398
440
 
441
+ function appFrameTargetOrigin() {
442
+ if (body.dataset.catalogMode !== "directory") return "*";
443
+ const candidates = [openStartUrl, openUrl];
444
+ for (const candidate of candidates) {
445
+ if (!candidate) continue;
446
+ try {
447
+ const origin = new URL(candidate, window.location.href).origin;
448
+ if (origin && origin !== "null") return origin;
449
+ // coercion-ok: skip a malformed optional URL so the next host URL can be tried.
450
+ } catch {}
451
+ }
452
+ return "";
453
+ }
454
+
455
+ function isTrustedAppFrameOrigin(origin) {
456
+ return (
457
+ body.dataset.catalogMode !== "directory" ||
458
+ origin === "null" ||
459
+ (!!origin && origin === appFrameTargetOrigin())
460
+ );
461
+ }
462
+
399
463
  function sendToAppFrame(message) {
400
464
  if (!appFrame || !appFrame.contentWindow) return;
465
+ // ChatGPT sandboxes nested app frames to opaque origins. Only non-secret bridge data is sent here.
401
466
  try { appFrame.contentWindow.postMessage(message, "*"); } catch {}
402
467
  }
403
468
 
@@ -472,7 +537,7 @@ export function embedApp(options = {}) {
472
537
  const originPayload = { type: "agentNative.frameOrigin", origin: window.location.origin };
473
538
  frameReadyMessageDelays.forEach((delay) => {
474
539
  setTimeout(() => {
475
- try { frame.contentWindow && frame.contentWindow.postMessage(originPayload, "*"); } catch {}
540
+ if (frame.contentWindow) frame.contentWindow.postMessage(originPayload, "*");
476
541
  sendHostContext();
477
542
  }, delay);
478
543
  });
@@ -492,20 +557,52 @@ export function embedApp(options = {}) {
492
557
  }
493
558
  }
494
559
 
495
- function embedSessionArgsFor(value) {
496
- const chrome = typeof toolInput.chrome === "string" ? toolInput.chrome : "full";
560
+ function embedTicketFromStartUrl(value) {
561
+ if (typeof value !== "string" || !value) return null;
562
+ const url = new URL(value, window.location.href);
563
+ return url.pathname.endsWith("/_agent-native/embed/start")
564
+ ? url.searchParams.get("ticket") || null
565
+ : null;
566
+ }
567
+
568
+ function rememberActiveEmbedSessionTicket(value) {
569
+ const ticket = embedTicketFromStartUrl(value);
570
+ if (ticket) activeEmbedSessionTicket = ticket;
571
+ }
572
+
573
+ function embedSessionArgsFor(
574
+ value,
575
+ renewInPlace = false,
576
+ renewalSourceTicket = "",
577
+ ) {
497
578
  if (body.dataset.catalogMode === "directory") {
498
- const widgetSource = toolResponseMetadata["agent-native/widgetSource"];
499
- const sourceTool = widgetSource && typeof widgetSource.toolName === "string"
500
- ? widgetSource.toolName
501
- : undefined;
579
+ const widgetSource = objectValue(
580
+ toolResponseMetadata["agent-native/widgetSource"],
581
+ );
582
+ const originalSourceTicket =
583
+ typeof widgetSource.sourceTicket === "string" &&
584
+ widgetSource.sourceTicket.trim()
585
+ ? widgetSource.sourceTicket
586
+ : embedTicketFromStartUrl(openStartUrl || value);
587
+ const sourceTicket = renewInPlace
588
+ ? typeof renewalSourceTicket === "string" &&
589
+ renewalSourceTicket.trim()
590
+ ? renewalSourceTicket
591
+ : null
592
+ : originalSourceTicket;
593
+ if (typeof sourceTicket !== "string" || !sourceTicket.trim()) {
594
+ throw new Error(
595
+ renewInPlace
596
+ ? "The active widget session ticket is unavailable."
597
+ : "The original widget session ticket is unavailable.",
598
+ );
599
+ }
502
600
  return {
503
- ...(sourceTool ? { sourceTool } : {}),
504
- toolInput,
505
- toolOutput: toolResultData,
506
- chrome
601
+ sourceTicket,
602
+ ...(renewInPlace ? { renewInPlace: true } : {}),
507
603
  };
508
604
  }
605
+ const chrome = typeof toolInput.chrome === "string" ? toolInput.chrome : "full";
509
606
  return typeof value === "string" && value.startsWith("/")
510
607
  ? { path: value, chrome }
511
608
  : { url: value, chrome };
@@ -995,6 +1092,7 @@ export function embedApp(options = {}) {
995
1092
  }
996
1093
 
997
1094
  async function transplantAppDocument(src) {
1095
+ rememberActiveEmbedSessionTicket(src);
998
1096
  clearFrameReadyTimer();
999
1097
  clearFrameLoadTimer();
1000
1098
  appFrame = null;
@@ -1100,11 +1198,16 @@ export function embedApp(options = {}) {
1100
1198
  const context = hostState().context || {};
1101
1199
  const nextMode = context.displayMode === "fullscreen" ? "inline" : "fullscreen";
1102
1200
  const supported = supportedDisplayMode(nextMode);
1103
- displayButton.hidden = !supported;
1201
+ displayButton.hidden = !supported || (fillsPane && nextMode === "fullscreen");
1104
1202
  displayButton.disabled = !supported;
1105
1203
  displayButton.textContent = nextMode === "fullscreen" ? "Fullscreen" : "Inline";
1106
1204
  displayButton.onclick = () => {
1107
1205
  if (!supportedDisplayMode(nextMode)) return;
1206
+ if (fillsPane && nextMode === "inline") {
1207
+ directoryWidgetOpenRequested = false;
1208
+ updateDirectoryWidgetLayout();
1209
+ notifyHostHeight();
1210
+ }
1108
1211
  void requestHostDisplayMode(nextMode).catch((err) => {
1109
1212
  console.warn("[agent-native] MCP host rejected display mode request", err);
1110
1213
  });
@@ -1355,6 +1458,7 @@ export function embedApp(options = {}) {
1355
1458
  }
1356
1459
 
1357
1460
  function renderFrame(src) {
1461
+ rememberActiveEmbedSessionTicket(src);
1358
1462
  clearFrameReadyTimer();
1359
1463
  clearFrameLoadTimer();
1360
1464
  const frame = document.createElement("iframe");
@@ -1364,8 +1468,10 @@ export function embedApp(options = {}) {
1364
1468
  appFrame = frame;
1365
1469
  appFrameReady = false;
1366
1470
  lastFrameSrc = src;
1471
+ appFrameDocumentState.loadGeneration += 1;
1367
1472
  frame.addEventListener("load", () => {
1368
1473
  if (appFrame !== frame) return;
1474
+ appFrameDocumentState.loadGeneration += 1;
1369
1475
  clearFrameLoadTimer();
1370
1476
  notifyOuterMcpAppReady();
1371
1477
  sendFrameReadyMessages(frame);
@@ -1404,6 +1510,83 @@ export function embedApp(options = {}) {
1404
1510
  void launchEmbed();
1405
1511
  }
1406
1512
 
1513
+ async function renewExpiredEmbedSession(
1514
+ requestId,
1515
+ frame,
1516
+ frameLoadGeneration,
1517
+ ) {
1518
+ const directoryMode = body.dataset.catalogMode === "directory";
1519
+ const canDeliverRenewal = () =>
1520
+ frame === appFrame &&
1521
+ (!directoryMode ||
1522
+ appFrameDocumentState.loadGeneration === frameLoadGeneration);
1523
+ const sendRenewalResult = (message) => {
1524
+ if (!canDeliverRenewal()) return;
1525
+ sendToAppFrame(message);
1526
+ };
1527
+ if (!requestId || !frame || !canDeliverRenewal()) return;
1528
+ if (!openUrl) {
1529
+ sendRenewalResult({
1530
+ type: "agentNative.embedSessionRenewed",
1531
+ data: { requestId, ok: false },
1532
+ });
1533
+ return;
1534
+ }
1535
+ if (embedSessionRefreshAttempts >= maxEmbedSessionRefreshAttempts) {
1536
+ sendRenewalResult({
1537
+ type: "agentNative.embedSessionRenewed",
1538
+ data: { requestId, ok: false },
1539
+ });
1540
+ return;
1541
+ }
1542
+
1543
+ embedSessionRefreshAttempts += 1;
1544
+ try {
1545
+ const result = await callEmbedSessionTool(
1546
+ embedSessionArgsFor(
1547
+ directoryMode ? openStartUrl || openUrl : openUrl,
1548
+ directoryMode,
1549
+ activeEmbedSessionTicket,
1550
+ ),
1551
+ );
1552
+ const data = parseToolResult(result);
1553
+ if (directoryMode) {
1554
+ if (data.renewed !== true) {
1555
+ sendRenewalResult({
1556
+ type: "agentNative.embedSessionRenewed",
1557
+ data: { requestId, ok: false },
1558
+ });
1559
+ return;
1560
+ }
1561
+ sendRenewalResult({
1562
+ type: "agentNative.embedSessionRenewed",
1563
+ data: { requestId, ok: true },
1564
+ });
1565
+ return;
1566
+ }
1567
+ if (typeof data.startUrl !== "string" || !data.startUrl) {
1568
+ sendRenewalResult({
1569
+ type: "agentNative.embedSessionRenewed",
1570
+ data: { requestId, ok: false },
1571
+ });
1572
+ return;
1573
+ }
1574
+ sendRenewalResult({
1575
+ type: "agentNative.embedSessionRenewed",
1576
+ data: {
1577
+ requestId,
1578
+ ok: true,
1579
+ startUrl: withChatBridgeParam(data.startUrl),
1580
+ },
1581
+ });
1582
+ } catch {
1583
+ sendRenewalResult({
1584
+ type: "agentNative.embedSessionRenewed",
1585
+ data: { requestId, ok: false },
1586
+ });
1587
+ }
1588
+ }
1589
+
1407
1590
  function shouldSelfNavigateToApp() {
1408
1591
  const render = renderModeSource();
1409
1592
  const mode = render.mode;
@@ -1464,6 +1647,7 @@ export function embedApp(options = {}) {
1464
1647
  }
1465
1648
 
1466
1649
  function navigateToAppFrame(src) {
1650
+ rememberActiveEmbedSessionTicket(src);
1467
1651
  if (isCurrentFrameUrl(src)) {
1468
1652
  clearFrameReadyTimer();
1469
1653
  clearFrameLoadTimer();
@@ -1552,8 +1736,12 @@ export function embedApp(options = {}) {
1552
1736
  }
1553
1737
 
1554
1738
  function notifyHostHeight() {
1555
- if (applyHostFillMode()) return;
1556
- const height = fillsPane
1739
+ updateDirectoryWidgetLayout();
1740
+ const compactInline = isCompactDirectoryWidget();
1741
+ if (applyHostFillMode() && !compactInline) return;
1742
+ const height = compactInline
1743
+ ? ${MCP_APP_INLINE_LAUNCHER_HEIGHT}
1744
+ : fillsPane
1557
1745
  ? paneFillHeight(hostState().context || {})
1558
1746
  : applyIntrinsicHeight(visibleIntrinsicHeight());
1559
1747
  if (!openAiBridge || typeof openAiBridge.notifyIntrinsicHeight !== "function") {
@@ -1787,6 +1975,7 @@ export function embedApp(options = {}) {
1787
1975
  event.source === null &&
1788
1976
  (message.embedStartUrl === appFrame?.src ||
1789
1977
  message.embedStartUrl === lastFrameSrc);
1978
+ if (!isTrustedAppFrameOrigin(event.origin)) return;
1790
1979
  if (
1791
1980
  !appFrame ||
1792
1981
  (!expiredSessionMessage && event.source !== appFrame.contentWindow)
@@ -1812,7 +2001,23 @@ export function embedApp(options = {}) {
1812
2001
  return;
1813
2002
  }
1814
2003
  if (message.type === "agentNative.embedSessionExpired") {
1815
- refreshExpiredEmbedSession();
2004
+ if (expiredSessionMessage) {
2005
+ refreshExpiredEmbedSession();
2006
+ } else if (typeof data.requestId === "string") {
2007
+ void renewExpiredEmbedSession(
2008
+ data.requestId,
2009
+ appFrame,
2010
+ appFrameDocumentState.loadGeneration,
2011
+ );
2012
+ } else if (body.dataset.catalogMode !== "directory") {
2013
+ // Older app frames do not include a renewal id, so preserve their
2014
+ // existing full-refresh fallback until they load the renewal client.
2015
+ refreshExpiredEmbedSession();
2016
+ }
2017
+ return;
2018
+ }
2019
+ if (message.type === "agentNative.embedSessionRenewalApplied") {
2020
+ embedSessionRefreshAttempts = 0;
1816
2021
  return;
1817
2022
  }
1818
2023
  if (message.type === "agentNative.submitChat") {
@@ -1853,6 +2058,11 @@ export function embedApp(options = {}) {
1853
2058
  }
1854
2059
 
1855
2060
  async function launchEmbed() {
2061
+ updateDirectoryWidgetLayout();
2062
+ if (isCompactDirectoryWidget()) {
2063
+ notifyHostHeight();
2064
+ return;
2065
+ }
1856
2066
  let launchUrl = openStartUrl || openUrl;
1857
2067
  if (!launchUrl) {
1858
2068
  renderAppLaunchError("Open link was not available.");
@@ -1940,12 +2150,47 @@ export function embedApp(options = {}) {
1940
2150
  function updateOpenButton() {
1941
2151
  const buttonUrl = openUrl;
1942
2152
  openButton.disabled = !buttonUrl;
2153
+ openButton.textContent = body.dataset.openLabel || "Open in app";
1943
2154
  openButton.onclick = () => {
1944
- if (buttonUrl) void openHostLink({ url: buttonUrl });
2155
+ if (!buttonUrl) return;
2156
+ if (fillsPane) {
2157
+ void openDirectoryWidget();
2158
+ } else {
2159
+ void openHostLink({ url: buttonUrl });
2160
+ }
1945
2161
  };
1946
2162
  updateHostOpenInAppUrl();
1947
2163
  }
1948
2164
 
2165
+ async function openDirectoryWidget() {
2166
+ directoryWidgetOpenRequested = true;
2167
+ updateDirectoryWidgetLayout();
2168
+ const displayMode = hostState().context?.displayMode;
2169
+ if (displayMode !== "fullscreen") {
2170
+ if (!supportedDisplayMode("fullscreen")) {
2171
+ notifyHostHeight();
2172
+ await launchEmbed();
2173
+ return;
2174
+ }
2175
+ try {
2176
+ const granted = await requestHostDisplayMode("fullscreen");
2177
+ if (granted?.mode !== "fullscreen") {
2178
+ notifyHostHeight();
2179
+ await launchEmbed();
2180
+ return;
2181
+ }
2182
+ } catch (err) {
2183
+ console.warn("[agent-native] MCP host could not open the widget fullscreen", err);
2184
+ notifyHostHeight();
2185
+ await launchEmbed();
2186
+ return;
2187
+ }
2188
+ }
2189
+ updateDirectoryWidgetLayout();
2190
+ notifyHostHeight();
2191
+ await launchEmbed();
2192
+ }
2193
+
1949
2194
  function updateTitle(data) {
1950
2195
  const record = objectValue(data);
1951
2196
  const openLink = objectValue(toolResponseMetadata["agent-native/openLink"]);
@@ -1993,7 +2238,7 @@ export function embedApp(options = {}) {
1993
2238
  openAiBridge = bridge;
1994
2239
  toolInput = objectValue(bridge.toolInput);
1995
2240
  const params = openAiToolResultParams(bridge);
1996
- toolResponseMetadata = objectValue(params._meta);
2241
+ toolResponseMetadata = toolResultMeta(params);
1997
2242
  const data = parseToolResult(params);
1998
2243
  toolResultData = objectValue(data);
1999
2244
  openUrl = openLinkFrom(params, data);
@@ -2026,6 +2271,7 @@ export function embedApp(options = {}) {
2026
2271
  }
2027
2272
  lastOpenAiSyncSignature = signature;
2028
2273
  updateTitle(data);
2274
+ updateDirectoryWidgetLayout();
2029
2275
  updateOpenButton();
2030
2276
  updateDisplayButton();
2031
2277
  notifyHostHeight();
@@ -2284,19 +2530,33 @@ export function embedApp(options = {}) {
2284
2530
  openUrl = openLinkFrom(params, data);
2285
2531
  openStartUrl = embedStartUrlFrom(params, data);
2286
2532
  updateTitle(data);
2533
+ updateDirectoryWidgetLayout();
2287
2534
  updateOpenButton();
2288
2535
  void launchEmbed();
2289
2536
  };
2290
2537
  app.onhostcontextchanged = () => {
2291
- updateDisplayButton();
2292
- notifyHostHeight();
2293
- sendHostContext();
2538
+ handleHostContextChanged();
2294
2539
  };
2295
2540
  await ensureHostAppConnected();
2296
2541
  notifyOuterMcpAppReady();
2542
+ updateDirectoryWidgetLayout();
2543
+ updateDisplayButton();
2544
+ notifyHostHeight();
2545
+ sendHostContext();
2546
+ }
2547
+
2548
+ function handleHostContextChanged() {
2549
+ updateDirectoryWidgetLayout();
2297
2550
  updateDisplayButton();
2298
2551
  notifyHostHeight();
2299
2552
  sendHostContext();
2553
+ if (
2554
+ fillsPane &&
2555
+ !isCompactDirectoryWidget() &&
2556
+ (openStartUrl || openUrl)
2557
+ ) {
2558
+ void launchEmbed();
2559
+ }
2300
2560
  }
2301
2561
 
2302
2562
  ${remoteBridgeFallbackEnabled
@@ -2318,16 +2578,16 @@ export function embedApp(options = {}) {
2318
2578
  openUrl = openLinkFrom(params, data);
2319
2579
  openStartUrl = embedStartUrlFrom(params, data);
2320
2580
  updateTitle(data);
2581
+ updateDirectoryWidgetLayout();
2321
2582
  updateOpenButton();
2322
2583
  void launchEmbed();
2323
2584
  };
2324
2585
  app.onhostcontextchanged = () => {
2325
- updateDisplayButton();
2326
- notifyHostHeight();
2327
- sendHostContext();
2586
+ handleHostContextChanged();
2328
2587
  };
2329
2588
  await ensureHostAppConnected();
2330
2589
  notifyOuterMcpAppReady();
2590
+ updateDirectoryWidgetLayout();
2331
2591
  updateDisplayButton();
2332
2592
  notifyHostHeight();
2333
2593
  sendHostContext();
@@ -796,6 +796,7 @@ async function issueTokenSet(params, tx) {
796
796
  orgDomain: params.orgDomain ?? null,
797
797
  scope: params.scope,
798
798
  resource: params.resource,
799
+ grantCreatedAtMs: params.grantCreatedAtMs,
799
800
  }, tx);
800
801
  const accessToken = await signMcpOAuthAccessToken(params);
801
802
  return {
@@ -858,6 +859,7 @@ async function handleAuthorizationCodeGrant(event, body) {
858
859
  scope: consumed.scope,
859
860
  resource: consumed.resource,
860
861
  issuer,
862
+ grantCreatedAtMs: consumed.createdAt,
861
863
  }, tx));
862
864
  });
863
865
  }
@@ -917,6 +919,7 @@ async function handleRefreshTokenGrant(event, body) {
917
919
  scope: existing.scope,
918
920
  resource: existing.resource,
919
921
  issuer,
922
+ grantCreatedAtMs: existing.grantCreatedAtMs,
920
923
  });
921
924
  return json({
922
925
  access_token: accessToken,
@@ -49,7 +49,7 @@ export interface OAuthRefreshTokenRow {
49
49
  orgDomain: string | null;
50
50
  scope: string;
51
51
  resource: string;
52
- createdAt: number | null;
52
+ grantCreatedAtMs: number | null;
53
53
  expiresAt: number | null;
54
54
  lastUsedAt: number | null;
55
55
  revokedAt: number | null;
@@ -87,6 +87,7 @@ export declare function createOAuthRefreshToken(params: {
87
87
  orgDomain?: string | null;
88
88
  scope: string;
89
89
  resource: string;
90
+ grantCreatedAtMs: number | null;
90
91
  }, db?: DbExec): Promise<OAuthRefreshTokenRow>;
91
92
  export declare function rotateOAuthRefreshToken(params: {
92
93
  oldRefreshToken: string;
@@ -205,7 +205,7 @@ function mapRefreshRow(row) {
205
205
  orgDomain: row.org_domain ?? row.orgDomain ?? null,
206
206
  scope: row.scope,
207
207
  resource: row.resource,
208
- createdAt: numOrNull(row.created_at ?? row.createdAt),
208
+ grantCreatedAtMs: numOrNull(row.created_at ?? row.createdAt),
209
209
  expiresAt: refreshTokenExpiryFromRow(row),
210
210
  lastUsedAt: numOrNull(row.last_used_at ?? row.lastUsedAt),
211
211
  revokedAt: numOrNull(row.revoked_at ?? row.revokedAt),
@@ -382,7 +382,6 @@ export async function createOAuthRefreshToken(params, db) {
382
382
  if (!db)
383
383
  await ensureTable();
384
384
  const client = db ?? getDbExec();
385
- const now = Date.now();
386
385
  const row = {
387
386
  id: randomUUID(),
388
387
  tokenHash: hashOAuthToken(params.refreshToken),
@@ -393,7 +392,8 @@ export async function createOAuthRefreshToken(params, db) {
393
392
  orgDomain: params.orgDomain ?? null,
394
393
  scope: params.scope,
395
394
  resource: params.resource,
396
- createdAt: now,
395
+ // This is the authorization grant's immutable timestamp, not row-creation time.
396
+ grantCreatedAtMs: params.grantCreatedAtMs,
397
397
  expiresAt: null,
398
398
  lastUsedAt: null,
399
399
  revokedAt: null,
@@ -411,7 +411,7 @@ export async function createOAuthRefreshToken(params, db) {
411
411
  row.orgDomain,
412
412
  row.scope,
413
413
  row.resource,
414
- row.createdAt,
414
+ row.grantCreatedAtMs,
415
415
  row.expiresAt,
416
416
  row.lastUsedAt,
417
417
  row.revokedAt,
@@ -454,7 +454,6 @@ export async function rotateOAuthRefreshToken(params, db) {
454
454
  ...old,
455
455
  id: randomUUID(),
456
456
  tokenHash: newHash,
457
- createdAt: now,
458
457
  expiresAt: null,
459
458
  lastUsedAt: null,
460
459
  revokedAt: null,
@@ -472,7 +471,7 @@ export async function rotateOAuthRefreshToken(params, db) {
472
471
  next.orgDomain,
473
472
  next.scope,
474
473
  next.resource,
475
- next.createdAt,
474
+ next.grantCreatedAtMs,
476
475
  next.expiresAt,
477
476
  next.lastUsedAt,
478
477
  next.revokedAt,
@@ -18,6 +18,7 @@ export interface McpOAuthAccessTokenClaims {
18
18
  scope: string;
19
19
  client_id: string;
20
20
  resource: string;
21
+ grant_created_at_ms?: number;
21
22
  jti?: string;
22
23
  typ: typeof MCP_OAUTH_TOKEN_TYPE;
23
24
  credential_version: typeof MCP_OAUTH_CREDENTIAL_VERSION | typeof MCP_OAUTH_SERVICE_CREDENTIAL_VERSION;
@@ -36,6 +37,8 @@ export declare function signMcpOAuthAccessToken(params: {
36
37
  scope: string;
37
38
  resource: string;
38
39
  issuer: string;
40
+ /** Immutable server-recorded creation time of the OAuth grant, in ms. */
41
+ grantCreatedAtMs?: number | null;
39
42
  jti?: string;
40
43
  expiresIn?: string | number;
41
44
  catalogScope?: "full";
@@ -49,6 +52,8 @@ export declare function verifyMcpOAuthAccessToken(token: string, resource: strin
49
52
  scopes: string[];
50
53
  clientId: string;
51
54
  jti?: string;
55
+ /** Immutable OAuth grant creation time, in milliseconds. */
56
+ grantCreatedAtMs?: number;
52
57
  catalogScope?: "full";
53
58
  /** `iat`, in seconds. */
54
59
  issuedAt?: number;