@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
@@ -16,7 +16,7 @@ import { buildDeepLink, isAgentNativeOpenUrl, isSameOriginUrl, toAbsoluteOpenUrl
16
16
  import { getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
17
17
  import { agentNativeMcpInstructions, agentNativeToolTitle, } from "../shared/agent-mcp-metadata.js";
18
18
  import { withCollapsedAgentSidebarParam } from "../shared/agent-sidebar-url.js";
19
- import { createMcpDirectoryWidgetReadCapability, MCP_APP_CHAT_BRIDGE_QUERY_PARAM, } from "../shared/embed-auth.js";
19
+ import { createMcpDirectoryWidgetReadCapability, createMcpDirectoryWidgetWriteCapability, MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS, MCP_APP_CHAT_BRIDGE_QUERY_PARAM, renewMcpDirectoryWidgetCapabilityScope, } from "../shared/embed-auth.js";
20
20
  import { describeMcpError, readClientInfoFromRequest, trackMcpResourceRead, trackMcpResourcesList, trackMcpToolCall, trackMcpToolsList, } from "./analytics.js";
21
21
  import { consumeMcpApprovalGrant, createMcpApprovalGrant, } from "./approval-store.js";
22
22
  import { getBuiltinCrossAppTools } from "./builtin-tools.js";
@@ -199,6 +199,13 @@ export function selectMcpDirectoryWidgetReadActions(profile, actions) {
199
199
  .map((name) => [name, actions[name]])
200
200
  .filter((entry) => Boolean(entry[1])));
201
201
  }
202
+ export function selectMcpDirectoryWidgetWriteActions(profile, actions) {
203
+ if (!profile)
204
+ return undefined;
205
+ return Object.fromEntries(Object.keys(profile.widgetWriteActionArguments ?? {})
206
+ .map((name) => [name, actions[name]])
207
+ .filter((entry) => Boolean(entry[1])));
208
+ }
202
209
  export function getConfiguredMcpOwnerEmail() {
203
210
  return getAppConfig().auth.mcpOwnerEmail;
204
211
  }
@@ -349,6 +356,45 @@ export function validateMcpDirectoryProfile(config, sourceActions = config.produ
349
356
  throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget read route "${name}" must be an explicitly scoped read-only GET action with valid resource arguments in the connector allowlist or hidden-read profiles.`);
350
357
  }
351
358
  }
359
+ for (const [name, argumentMap] of Object.entries(profile?.widgetWriteActionArguments ?? {})) {
360
+ const entry = actions[name] ?? config.widgetWriteActions?.[name];
361
+ if (!entry ||
362
+ entry.http === false ||
363
+ entry.http?.method === "GET" ||
364
+ entry.readOnly === true ||
365
+ entry.requiresAuth === false ||
366
+ !entry.schema ||
367
+ typeof entry.schema !== "object" ||
368
+ !("~standard" in entry.schema) ||
369
+ Object.keys(argumentMap).length === 0 ||
370
+ !Object.values(argumentMap).some((rule) => typeof rule === "string" ||
371
+ (rule?.type === "actionSchemaResourceBound" &&
372
+ MCP_DIRECTORY_WIDGET_ARGUMENT.test(rule.resourceKey))) ||
373
+ Object.entries(argumentMap).some(([argumentName, argument]) => !MCP_DIRECTORY_WIDGET_ARGUMENT.test(argumentName) ||
374
+ (typeof argument === "string"
375
+ ? !MCP_DIRECTORY_WIDGET_ARGUMENT.test(argument)
376
+ : argument?.type === "actionSchema"
377
+ ? Object.keys(argument).length !== 1 ||
378
+ !entry.schema ||
379
+ typeof entry.schema !== "object" ||
380
+ !("~standard" in entry.schema)
381
+ : argument?.type === "actionSchemaResourceBound"
382
+ ? Object.keys(argument).length !== 2 ||
383
+ !MCP_DIRECTORY_WIDGET_ARGUMENT.test(argument.resourceKey) ||
384
+ !entry.schema ||
385
+ typeof entry.schema !== "object" ||
386
+ !("~standard" in entry.schema)
387
+ : !argument ||
388
+ argument.type !== "integerRange" ||
389
+ Object.keys(argument).length !== 3 ||
390
+ !Number.isSafeInteger(argument.min) ||
391
+ !Number.isSafeInteger(argument.max) ||
392
+ argument.min < 0 ||
393
+ argument.max < argument.min ||
394
+ argument.max > 5_000))) {
395
+ throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget write action "${name}" must be an authenticated mutation action with an exact resource ID binding and valid argument allowlist.`);
396
+ }
397
+ }
352
398
  }
353
399
  export function validateMcpDirectoryWidgetDomain(domain) {
354
400
  let validWidgetDomain = false;
@@ -746,36 +792,59 @@ function mcpAppEmbedOpenLinkMeta(result, resource, meta) {
746
792
  : {}),
747
793
  };
748
794
  }
795
+ function mcpDirectoryWidgetSourceMeta(toolName, embedMeta) {
796
+ const embedStart = metadataObject(embedMeta["agent-native/embedStart"]);
797
+ if (typeof embedStart.startUrl !== "string") {
798
+ return { "agent-native/widgetSource": { toolName } };
799
+ }
800
+ try {
801
+ const url = new URL(embedStart.startUrl, "https://agent-native.invalid");
802
+ const sourceTicket = url.pathname.includes("/_agent-native/embed/start")
803
+ ? url.searchParams.get("ticket")
804
+ : null;
805
+ return {
806
+ "agent-native/widgetSource": {
807
+ toolName,
808
+ ...(sourceTicket ? { sourceTicket } : {}),
809
+ },
810
+ };
811
+ }
812
+ catch {
813
+ return { "agent-native/widgetSource": { toolName } };
814
+ }
815
+ }
749
816
  async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl, directoryWidget) {
750
817
  if (!result || typeof result !== "object" || Array.isArray(result)) {
751
818
  return result;
752
819
  }
753
820
  const out = result;
754
- const restrictDirectoryWidgetRead = directoryWidget !== undefined;
821
+ const restrictDirectoryWidgetCapability = directoryWidget !== undefined;
755
822
  const resultWithoutExistingEmbedTicket = { ...out };
756
- if (restrictDirectoryWidgetRead) {
823
+ if (restrictDirectoryWidgetCapability) {
757
824
  delete resultWithoutExistingEmbedTicket.embedStartUrl;
758
825
  delete resultWithoutExistingEmbedTicket.embedTargetPath;
759
826
  delete resultWithoutExistingEmbedTicket.embedExpiresAt;
760
827
  }
761
828
  if (out.embed === false ||
762
- (!restrictDirectoryWidgetRead && out.embed !== true && !directoryLinkUrl)) {
763
- return restrictDirectoryWidgetRead
829
+ (!restrictDirectoryWidgetCapability &&
830
+ out.embed !== true &&
831
+ !directoryLinkUrl)) {
832
+ return restrictDirectoryWidgetCapability
764
833
  ? resultWithoutExistingEmbedTicket
765
834
  : result;
766
835
  }
767
- if (!restrictDirectoryWidgetRead &&
836
+ if (!restrictDirectoryWidgetCapability &&
768
837
  typeof out.embedStartUrl === "string" &&
769
838
  out.embedStartUrl.trim()) {
770
839
  return result;
771
840
  }
772
- if (!restrictDirectoryWidgetRead &&
841
+ if (!restrictDirectoryWidgetCapability &&
773
842
  typeof out.url === "string" &&
774
843
  out.url.trim() &&
775
844
  isEmbedStartUrl(out.url)) {
776
845
  return result;
777
846
  }
778
- const candidates = restrictDirectoryWidgetRead
847
+ const candidates = restrictDirectoryWidgetCapability
779
848
  ? [directoryWidget.targetPath]
780
849
  : out.embed === true
781
850
  ? [out.url, out.path, out.deepLinkUrl, directoryLinkUrl]
@@ -783,10 +852,10 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
783
852
  const candidate = candidates.find((value) => {
784
853
  if (typeof value !== "string" || value.trim().length === 0)
785
854
  return false;
786
- return !restrictDirectoryWidgetRead || !isEmbedStartUrl(value);
855
+ return !restrictDirectoryWidgetCapability || !isEmbedStartUrl(value);
787
856
  });
788
857
  if (!candidate) {
789
- return restrictDirectoryWidgetRead
858
+ return restrictDirectoryWidgetCapability
790
859
  ? resultWithoutExistingEmbedTicket
791
860
  : result;
792
861
  }
@@ -794,19 +863,19 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
794
863
  const isPath = trimmed.startsWith("/") && !trimmed.startsWith("//");
795
864
  const isAbsoluteHttp = /^https?:\/\//i.test(trimmed);
796
865
  if (!isPath && !isAbsoluteHttp) {
797
- return restrictDirectoryWidgetRead
866
+ return restrictDirectoryWidgetCapability
798
867
  ? resultWithoutExistingEmbedTicket
799
868
  : result;
800
869
  }
801
870
  if (isAbsoluteHttp && !meta?.origin) {
802
- return restrictDirectoryWidgetRead
871
+ return restrictDirectoryWidgetCapability
803
872
  ? resultWithoutExistingEmbedTicket
804
873
  : result;
805
874
  }
806
875
  const ctx = getRequestContext();
807
876
  const ownerEmail = ctx?.userEmail?.trim();
808
877
  if (!ownerEmail) {
809
- return restrictDirectoryWidgetRead
878
+ return restrictDirectoryWidgetCapability
810
879
  ? resultWithoutExistingEmbedTicket
811
880
  : result;
812
881
  }
@@ -814,30 +883,56 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
814
883
  const { buildEmbedStartPath } = await import("../server/embed-route.js");
815
884
  const targetPath = normalizeEmbedTargetPath(withMcpChatBridgeParam(trimmed), meta?.origin);
816
885
  if (!targetPath) {
817
- return restrictDirectoryWidgetRead
886
+ return restrictDirectoryWidgetCapability
818
887
  ? resultWithoutExistingEmbedTicket
819
888
  : result;
820
889
  }
821
- const scope = restrictDirectoryWidgetRead
822
- ? createMcpDirectoryWidgetReadCapability(directoryWidget.capability)
890
+ if (directoryWidget?.capability.mode === "write" &&
891
+ (directoryWidget.capability.value.userEmail.toLowerCase() !==
892
+ ownerEmail.toLowerCase() ||
893
+ (directoryWidget.capability.value.orgId ?? undefined) !==
894
+ (ctx?.orgId ?? undefined))) {
895
+ throw new Error("The widget write grant must match the authenticated MCP user and organization.");
896
+ }
897
+ const scope = restrictDirectoryWidgetCapability
898
+ ? directoryWidget.capability.mode === "write"
899
+ ? createMcpDirectoryWidgetWriteCapability(directoryWidget.capability.value)
900
+ : createMcpDirectoryWidgetReadCapability(directoryWidget.capability.value)
823
901
  : typeof out.chrome === "string"
824
902
  ? out.chrome
825
903
  : null;
826
- if (restrictDirectoryWidgetRead && !scope) {
827
- throw new Error("Could not create a valid read-only capability for this MCP directory widget.");
904
+ if (restrictDirectoryWidgetCapability && !scope) {
905
+ throw new Error("Could not create a valid scoped capability for this MCP directory widget.");
906
+ }
907
+ const revocationAnchorCandidate = ctx?.mcpCredentialIssuedAtMs;
908
+ const revocationAnchorCreatedAtMs = typeof revocationAnchorCandidate === "number" &&
909
+ Number.isSafeInteger(revocationAnchorCandidate)
910
+ ? revocationAnchorCandidate
911
+ : undefined;
912
+ if (restrictDirectoryWidgetCapability &&
913
+ revocationAnchorCreatedAtMs === undefined) {
914
+ throw new Error("Directory widget sessions require a trusted MCP credential issue time.");
828
915
  }
829
916
  const ticket = await createEmbedSessionTicket({
830
917
  ownerEmail,
831
918
  orgId: ctx?.orgId,
832
919
  targetPath,
833
920
  scope,
921
+ ...(directoryWidget?.capability.mode === "write"
922
+ ? { ttlSeconds: 5 * 60 }
923
+ : {}),
924
+ ...(restrictDirectoryWidgetCapability
925
+ ? { revocationAnchorCreatedAtMs }
926
+ : {}),
834
927
  });
835
928
  const startPath = buildEmbedStartPath(ticket.ticket);
836
929
  const embedStartUrl = meta?.origin
837
930
  ? new URL(startPath, meta.origin).toString()
838
931
  : startPath;
839
932
  return {
840
- ...(restrictDirectoryWidgetRead ? resultWithoutExistingEmbedTicket : out),
933
+ ...(restrictDirectoryWidgetCapability
934
+ ? resultWithoutExistingEmbedTicket
935
+ : out),
841
936
  embedStartUrl,
842
937
  embedTargetPath: targetPath,
843
938
  embedExpiresAt: ticket.expiresAt,
@@ -853,7 +948,7 @@ function withoutMcpAppEmbedTicket(result) {
853
948
  delete output.embedExpiresAt;
854
949
  return output;
855
950
  }
856
- function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, args, result, actions, oauthScopes) {
951
+ async function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, args, result, actions, identity) {
857
952
  const profile = config.directoryProfile;
858
953
  const resolveTarget = profile?.widgetTargets?.[toolName];
859
954
  if (!resolveTarget)
@@ -877,7 +972,7 @@ function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, args, r
877
972
  !scopedAuthenticatedRead) ||
878
973
  (entry.readOnly !== true &&
879
974
  !profile.widgetReadOnlyActions?.includes(actionName)) ||
880
- !isActionVisibleForOAuthScope(entry, oauthScopes, profile.widgetReadOnlyActions?.includes(actionName) === true) ||
975
+ !isActionVisibleForOAuthScope(entry, identity?.oauthScopes, profile.widgetReadOnlyActions?.includes(actionName) === true) ||
881
976
  entry.http === false ||
882
977
  entry.http?.method !== "GET" ||
883
978
  (entry.requiresAuth === false && !scopedPublicRead)) {
@@ -900,15 +995,100 @@ function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, args, r
900
995
  actionArguments[actionName] = scopedArguments;
901
996
  }
902
997
  }
903
- if (Object.keys(actionArguments).length === 0)
998
+ const writeActionArguments = {};
999
+ const targetWriteActions = new Set(target.writeActions ?? []);
1000
+ for (const actionName of targetWriteActions) {
1001
+ if (!Object.hasOwn(profile.widgetWriteActionArguments ?? {}, actionName)) {
1002
+ throw new Error(`MCP directory widget target references undeclared write action "${actionName}".`);
1003
+ }
1004
+ }
1005
+ for (const [actionName, argumentMap] of Object.entries(profile.widgetWriteActionArguments ?? {})) {
1006
+ if (!targetWriteActions.has(actionName))
1007
+ continue;
1008
+ const entry = actions[actionName] ?? config.widgetWriteActions?.[actionName];
1009
+ if (!entry ||
1010
+ entry.http === false ||
1011
+ entry.http?.method === "GET" ||
1012
+ entry.readOnly === true ||
1013
+ entry.requiresAuth === false ||
1014
+ !isActionVisibleForOAuthScope(entry, identity?.oauthScopes)) {
1015
+ continue;
1016
+ }
1017
+ const scopedArguments = {};
1018
+ for (const [argumentName, rule] of Object.entries(argumentMap)) {
1019
+ if (typeof rule === "string") {
1020
+ const resourceId = target.resourceIds[rule];
1021
+ if (typeof resourceId !== "string" || !resourceId.trim())
1022
+ break;
1023
+ scopedArguments[argumentName] = resourceId;
1024
+ }
1025
+ else {
1026
+ scopedArguments[argumentName] = rule;
1027
+ }
1028
+ }
1029
+ if (Object.keys(scopedArguments).length === Object.keys(argumentMap).length &&
1030
+ Object.values(scopedArguments).some((argument) => {
1031
+ if (typeof argument === "string") {
1032
+ return Object.values(target.resourceIds).includes(argument);
1033
+ }
1034
+ if (argument.type !== "actionSchemaResourceBound")
1035
+ return false;
1036
+ const resourceId = target.resourceIds[argument.resourceKey];
1037
+ return typeof resourceId === "string" && resourceId.trim().length > 0;
1038
+ })) {
1039
+ writeActionArguments[actionName] = scopedArguments;
1040
+ }
1041
+ }
1042
+ if (Object.keys(actionArguments).length === 0 &&
1043
+ Object.keys(writeActionArguments).length === 0) {
904
1044
  return undefined;
905
- return {
906
- targetPath: target.targetPath,
907
- capability: {
1045
+ }
1046
+ let editorAccessAllowed = false;
1047
+ if (Object.keys(writeActionArguments).length > 0 &&
1048
+ hasVerifiedMcpUserIdentity(identity) &&
1049
+ profile.authorizeWidgetWrite) {
1050
+ try {
1051
+ editorAccessAllowed = await profile.authorizeWidgetWrite({
1052
+ toolName,
1053
+ args,
1054
+ result,
1055
+ target,
1056
+ identity,
1057
+ });
1058
+ }
1059
+ catch {
1060
+ console.error(`[mcp] Directory widget editor authorization failed for ${toolName}; issuing a read-only capability.`);
1061
+ }
1062
+ }
1063
+ if (Object.keys(writeActionArguments).length > 0 &&
1064
+ hasVerifiedMcpUserIdentity(identity) &&
1065
+ editorAccessAllowed) {
1066
+ const requestOrgId = getRequestContext()?.orgId;
1067
+ const value = {
908
1068
  appId: config.appId ?? config.name,
909
1069
  resourceUri: resource.uri,
910
1070
  resourceIds: target.resourceIds,
911
- actionArguments,
1071
+ userEmail: identity.userEmail,
1072
+ ...(typeof requestOrgId === "string" ? { orgId: requestOrgId } : {}),
1073
+ expiresAtMs: Date.now() + MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS,
1074
+ readActionArguments: actionArguments,
1075
+ writeActionArguments,
1076
+ };
1077
+ return {
1078
+ targetPath: target.targetPath,
1079
+ capability: { mode: "write", value },
1080
+ };
1081
+ }
1082
+ return {
1083
+ targetPath: target.targetPath,
1084
+ capability: {
1085
+ mode: "read",
1086
+ value: {
1087
+ appId: config.appId ?? config.name,
1088
+ resourceUri: resource.uri,
1089
+ resourceIds: target.resourceIds,
1090
+ actionArguments,
1091
+ },
912
1092
  },
913
1093
  };
914
1094
  }
@@ -923,21 +1103,25 @@ function mcpDirectoryWidgetSessionTool(config) {
923
1103
  }
924
1104
  return {
925
1105
  name: "create_embed_session",
926
- description: "Refresh the authenticated embed session for this app widget after it is reopened.",
1106
+ description: "Creates a short-lived session for the same app widget using its original server-issued result ticket.",
927
1107
  inputSchema: {
928
1108
  type: "object",
929
1109
  properties: {
930
- sourceTool: { type: "string", enum: sourceNames },
931
- toolInput: { type: "object", additionalProperties: true },
932
- toolOutput: { type: "object", additionalProperties: true },
933
- chrome: { type: "string", enum: ["full", "minimal"] },
1110
+ sourceTicket: {
1111
+ type: "string",
1112
+ description: "Original server-issued widget ticket from this result.",
1113
+ },
1114
+ renewInPlace: {
1115
+ type: "boolean",
1116
+ description: "Extend the active scoped widget session without replacing its mounted app frame.",
1117
+ },
934
1118
  },
935
- required: ["toolInput", "toolOutput"],
1119
+ required: ["sourceTicket"],
936
1120
  },
937
1121
  _meta: { ui: { visibility: ["app"] } },
938
1122
  };
939
1123
  }
940
- async function renewMcpDirectoryWidgetEmbedSession(config, actions, args, identity, requestMeta) {
1124
+ async function renewMcpDirectoryWidgetEmbedSession(config, args, identity) {
941
1125
  const profile = config.directoryProfile;
942
1126
  if (!profile || config.catalogMode !== "directory") {
943
1127
  throw new Error("Directory widget session renewal is not enabled.");
@@ -945,66 +1129,76 @@ async function renewMcpDirectoryWidgetEmbedSession(config, actions, args, identi
945
1129
  if (!hasVerifiedMcpUserIdentity(identity)) {
946
1130
  throw new Error("Widget session renewal requires a verified user caller.");
947
1131
  }
948
- const sourceTool = typeof args.sourceTool === "string" && args.sourceTool.trim()
949
- ? args.sourceTool.trim()
950
- : undefined;
951
- const toolInput = metadataObject(args.toolInput);
952
- const toolOutput = metadataObject(args.toolOutput);
953
- const sourceNames = sourceTool
954
- ? [sourceTool]
955
- : Object.keys(profile.widgetTargets ?? {});
956
- const { normalizeEmbedTargetPath, createEmbedSessionTicket } = await import("../server/embed-session.js");
957
- const { buildEmbedStartPath } = await import("../server/embed-route.js");
958
- const candidates = [];
959
- for (const name of sourceNames) {
960
- if (!profile.connectorCatalog.includes(name))
961
- continue;
962
- const entry = actions[name];
963
- const resolver = profile.widgetTargets?.[name];
964
- if (!entry?.mcpApp?.resource || !resolver)
965
- continue;
966
- const target = resolver(toolInput, toolOutput);
967
- if (!target)
968
- continue;
969
- const resource = await resolveMcpAppResourceSafely(config, name, entry, requestMeta);
970
- if (!resource)
971
- continue;
972
- const widgetCapability = mcpDirectoryWidgetCapabilityForTool(config, resource, name, toolInput, toolOutput, actions, identity.oauthScopes);
973
- if (!widgetCapability)
974
- continue;
975
- const targetPath = normalizeEmbedTargetPath(withMcpChatBridgeParam(target.targetPath));
976
- if (targetPath) {
977
- candidates.push({
978
- targetPath,
979
- capability: widgetCapability.capability,
980
- });
1132
+ const sourceTicket = args.sourceTicket;
1133
+ if (typeof sourceTicket !== "string") {
1134
+ throw new Error("Widget session renewal requires its original result ticket.");
1135
+ }
1136
+ const { createEmbedSessionTicket, readMcpDirectoryWidgetRenewalTicket, renewMcpDirectoryWidgetSession, } = await import("../server/embed-session.js");
1137
+ const originalTicket = await readMcpDirectoryWidgetRenewalTicket(sourceTicket);
1138
+ const callerOrgId = getRequestContext()?.orgId;
1139
+ if (!originalTicket ||
1140
+ originalTicket.ownerEmail.trim().toLowerCase() !==
1141
+ identity.userEmail.trim().toLowerCase() ||
1142
+ (originalTicket.orgId ?? undefined) !== (callerOrgId ?? undefined)) {
1143
+ throw new Error("The original widget ticket is not available to this caller.");
1144
+ }
1145
+ const appId = config.appId ?? config.name;
1146
+ const renewalNow = Date.now();
1147
+ const capabilityExpiresAtMs = Math.min(renewalNow + MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS, originalTicket.renewalExpiresAtMs);
1148
+ const scope = renewMcpDirectoryWidgetCapabilityScope(originalTicket.scope, {
1149
+ appId,
1150
+ resourceUri: getMcpDirectoryWidgetResourceUri(appId),
1151
+ userEmail: identity.userEmail,
1152
+ orgId: callerOrgId,
1153
+ expiresAtMs: capabilityExpiresAtMs,
1154
+ readAllowed: hasMcpOAuthScope(identity.oauthScopes, "mcp:read"),
1155
+ writeAllowed: hasMcpOAuthScope(identity.oauthScopes, "mcp:write"),
1156
+ });
1157
+ if (!scope) {
1158
+ throw new Error("The original widget ticket has an invalid app capability.");
1159
+ }
1160
+ if (args.renewInPlace === true) {
1161
+ if (originalTicket.consumedAtMs === null ||
1162
+ originalTicket.sessionActiveUntilMs === null) {
1163
+ throw new Error("The original widget session cannot be renewed in place.");
981
1164
  }
1165
+ const expiresAt = await renewMcpDirectoryWidgetSession({
1166
+ sourceTicket,
1167
+ ownerEmail: identity.userEmail,
1168
+ orgId: callerOrgId,
1169
+ expectedScope: originalTicket.scope,
1170
+ renewedScope: scope,
1171
+ });
1172
+ if (!expiresAt) {
1173
+ throw new Error("The original widget session can no longer be renewed in place.");
1174
+ }
1175
+ return { renewed: true, expiresAt };
982
1176
  }
983
- const distinctCandidates = new Map(candidates.map((candidate) => [
984
- JSON.stringify([candidate.targetPath, candidate.capability]),
985
- candidate,
986
- ]));
987
- if (distinctCandidates.size !== 1) {
988
- throw new Error("Could not resolve one authorized app resource from this saved widget result.");
1177
+ const appOrigin = profile.widgetDomain ?? config.widgetDomain;
1178
+ if (!appOrigin) {
1179
+ throw new Error("Directory widget session renewal requires an app origin.");
989
1180
  }
990
- const [{ targetPath, capability }] = [...distinctCandidates.values()];
991
- const scope = createMcpDirectoryWidgetReadCapability(capability);
992
- if (!scope) {
993
- throw new Error("Could not create a scoped app widget session.");
1181
+ let appOriginUrl;
1182
+ try {
1183
+ appOriginUrl = new URL(appOrigin);
994
1184
  }
1185
+ catch {
1186
+ throw new Error("Directory widget session renewal has an invalid origin.");
1187
+ }
1188
+ const { buildEmbedStartPath } = await import("../server/embed-route.js");
1189
+ const targetPath = withMcpChatBridgeParam(originalTicket.targetPath);
995
1190
  const ticket = await createEmbedSessionTicket({
996
- ownerEmail: identity.userEmail.trim(),
997
- orgId: getRequestOrgId(),
1191
+ ownerEmail: identity.userEmail,
1192
+ orgId: callerOrgId,
998
1193
  targetPath,
999
1194
  scope,
1195
+ ttlSeconds: Math.max(1, Math.ceil((capabilityExpiresAtMs - renewalNow) / 1000)),
1196
+ renewalExpiresAtMs: originalTicket.renewalExpiresAtMs,
1197
+ revocationAnchorCreatedAtMs: originalTicket.createdAtMs,
1000
1198
  });
1001
1199
  const startPath = buildEmbedStartPath(ticket.ticket);
1002
- const appOrigin = profile.widgetDomain ?? config.widgetDomain;
1003
- if (!appOrigin) {
1004
- throw new Error("Widget session renewal has no configured app origin.");
1005
- }
1006
1200
  return {
1007
- startUrl: new URL(startPath, appOrigin).toString(),
1201
+ startUrl: new URL(startPath, appOriginUrl).toString(),
1008
1202
  targetPath,
1009
1203
  expiresAt: ticket.expiresAt,
1010
1204
  };
@@ -1111,7 +1305,7 @@ function safeUiSegment(value, fallback) {
1111
1305
  return normalized || fallback;
1112
1306
  }
1113
1307
  const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v65";
1114
- const MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION = "shell-v68";
1308
+ const MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION = "shell-v69";
1115
1309
  export function getMcpDirectoryWidgetResourceUri(appId, fallback = "agent-native") {
1116
1310
  const app = safeUiSegment(appId, fallback);
1117
1311
  return (versionMcpAppResourceUri(`ui://${app}/${MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION}`, MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION)?.uri ?? `ui://${app}/${MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION}`);
@@ -1775,6 +1969,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
1775
1969
  : {}),
1776
1970
  ...(requestMeta?.origin ? { requestOrigin: requestMeta.origin } : {}),
1777
1971
  ...(mcpRequestId ? { mcpRequestId } : {}),
1972
+ mcpCredentialIssuedAtMs: effectiveIdentity?.mcpCredentialIssuedAtMs ?? null,
1778
1973
  }, fn);
1779
1974
  }
1780
1975
  async function requireMcpActionApproval(entry, name, args, ctx) {
@@ -2148,7 +2343,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2148
2343
  name === "create_embed_session" &&
2149
2344
  mcpDirectoryWidgetSessionTool(config)) {
2150
2345
  try {
2151
- const renewed = await renewMcpDirectoryWidgetEmbedSession(config, visibleActions, metadataObject(args), effectiveIdentity, requestMeta);
2346
+ const renewed = await renewMcpDirectoryWidgetEmbedSession(config, metadataObject(args), effectiveIdentity);
2152
2347
  return {
2153
2348
  content: [
2154
2349
  { type: "text", text: "Widget session ready." },
@@ -2234,8 +2429,12 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2234
2429
  });
2235
2430
  directoryLinkUrl = linked?.url ?? undefined;
2236
2431
  }
2237
- const directoryWidget = directoryCatalog && mcpAppResourceCandidate
2238
- ? mcpDirectoryWidgetCapabilityForTool(config, mcpAppResourceCandidate, name, args ?? {}, projectedRawResult, visibleActions, effectiveIdentity?.oauthScopes)
2432
+ const trustedCredentialIssuedAtMs = effectiveIdentity?.mcpCredentialIssuedAtMs;
2433
+ const directoryWidget = directoryCatalog &&
2434
+ mcpAppResourceCandidate &&
2435
+ typeof trustedCredentialIssuedAtMs === "number" &&
2436
+ Number.isSafeInteger(trustedCredentialIssuedAtMs)
2437
+ ? await mcpDirectoryWidgetCapabilityForTool(config, mcpAppResourceCandidate, name, args ?? {}, projectedRawResult, visibleActions, effectiveIdentity)
2239
2438
  : undefined;
2240
2439
  const missingDirectoryWidgetCapability = directoryCatalog &&
2241
2440
  config.directoryProfile !== undefined &&
@@ -2261,14 +2460,15 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2261
2460
  : null;
2262
2461
  const embedProducedNothing = !!mcpAppResourceCandidate && !mcpResultIsError && !embedHasContent;
2263
2462
  const { block, _meta } = buildLinkArtifacts(entry, args ?? {}, actionResultForClient, requestMeta);
2463
+ const mcpAppOpenLinkMeta = mcpAppResource
2464
+ ? mcpAppEmbedOpenLinkMeta(actionResultForClient, mcpAppResource, requestMeta)
2465
+ : {};
2264
2466
  const responseMeta = {
2265
2467
  ...(_meta ?? {}),
2266
2468
  ...(directoryCatalog && mcpAppResource
2267
- ? { "agent-native/widgetSource": { toolName: name } }
2268
- : {}),
2269
- ...(mcpAppResource
2270
- ? mcpAppEmbedOpenLinkMeta(actionResultForClient, mcpAppResource, requestMeta)
2469
+ ? mcpDirectoryWidgetSourceMeta(name, mcpAppOpenLinkMeta)
2271
2470
  : {}),
2471
+ ...mcpAppOpenLinkMeta,
2272
2472
  ...(mcpAppResource
2273
2473
  ? openAiToolResultMeta(mcpAppResource, directoryCatalog)
2274
2474
  : {}),
@@ -2759,6 +2959,9 @@ async function verifyIssuedMcpCredential(token, resourceUrl) {
2759
2959
  oauthScopes: oauth.scopes,
2760
2960
  oauthClientId: oauth.clientId,
2761
2961
  ...(oauth.catalogScope ? { catalogScope: oauth.catalogScope } : {}),
2962
+ ...(oauth.grantCreatedAtMs !== undefined
2963
+ ? { grantCreatedAtMs: oauth.grantCreatedAtMs }
2964
+ : {}),
2762
2965
  issuedAt: oauth.issuedAt,
2763
2966
  };
2764
2967
  }
@@ -2793,11 +2996,24 @@ async function admitIssuedMcpCredential(credential, requestOrigin) {
2793
2996
  stored = lookup;
2794
2997
  }
2795
2998
  const orgId = credential.orgId !== undefined ? credential.orgId : stored?.orgId;
2999
+ const credentialIssuedAtMs = credential.oauthClientId !== undefined
3000
+ ? credential.grantCreatedAtMs
3001
+ : typeof credential.issuedAt === "number" &&
3002
+ Number.isSafeInteger(credential.issuedAt)
3003
+ ? credential.issuedAt * 1000
3004
+ : undefined;
3005
+ const mcpCredentialIssuedAtMs = credentialIssuedAtMs !== undefined &&
3006
+ Number.isSafeInteger(credentialIssuedAtMs)
3007
+ ? credentialIssuedAtMs
3008
+ : undefined;
2796
3009
  const admitted = await admitIssuedCredential({
2797
3010
  authed: true,
2798
3011
  identity: {
2799
3012
  userEmail: credential.userEmail,
2800
3013
  identityAssurance: stored?.kind === "service" ? "service" : "user",
3014
+ ...(mcpCredentialIssuedAtMs !== undefined
3015
+ ? { mcpCredentialIssuedAtMs }
3016
+ : {}),
2801
3017
  ...(orgId !== undefined ? { orgId } : {}),
2802
3018
  orgDomain: credential.orgDomain,
2803
3019
  ...(credential.oauthScopes
@@ -186,6 +186,7 @@ async function signConnectToken(params) {
186
186
  scope: MCP_OAUTH_DEFAULT_SCOPE,
187
187
  resource: mcpResourceUrl(params.issuer),
188
188
  issuer: params.issuer,
189
+ grantCreatedAtMs: Date.now(),
189
190
  jti: params.jti,
190
191
  expiresIn: params.expiresIn,
191
192
  ...(params.catalogScope === "full" ? { catalogScope: "full" } : {}),