@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
@@ -9,18 +9,20 @@ import { isTransientDatabaseError } from "../db/client.js";
9
9
  import { declaresFeatureFlagDelegation } from "../feature-flags/a2a-action-route.js";
10
10
  import { isFeatureFlagAdminEmail } from "../feature-flags/permissions.js";
11
11
  import { isFederationMembershipValidatedForEvent, resolveOrgIdForEmail, } from "../org/context.js";
12
+ import { checkWorkspaceAppAccessForRequest } from "../org/workspace-app-access-request.js";
13
+ import { WORKSPACE_APP_ACCESS_UNAVAILABLE, WORKSPACE_APP_ACCESS_UNAVAILABLE_MESSAGE, } from "../org/workspace-app-access.js";
12
14
  import { LLM_PROVIDER_MISSING_ERROR_CODE, LLM_PROVIDER_MISSING_STATUS, } from "../shared/action-error-codes.js";
13
15
  import { agentNativeMcpInstructions, agentNativeToolTitle, } from "../shared/agent-mcp-metadata.js";
14
- import { EMBED_TARGET_HEADER, EMBED_TARGET_QUERY_PARAM, EMBED_TOKEN_QUERY_PARAM, allowsMcpDirectoryWidgetReadAction, isMcpDirectoryWidgetReadCapabilityScope, normalizeMcpDirectoryWidgetReadActionArguments, } from "../shared/embed-auth.js";
16
+ import { EMBED_TARGET_HEADER, EMBED_TARGET_QUERY_PARAM, EMBED_TOKEN_QUERY_PARAM, MCP_DIRECTORY_WIDGET_SESSION_EXPIRED_HEADER, allowsMcpDirectoryWidgetReadAction, getMcpDirectoryWidgetWriteCapabilityGrant, isExpiredMcpDirectoryWidgetWriteCapability, isMcpDirectoryWidgetCapabilityScope, isMcpDirectoryWidgetReadCapabilityScope, isMcpDirectoryWidgetWriteCapabilityScope, normalizeMcpDirectoryWidgetReadActionArguments, normalizeMcpDirectoryWidgetWriteActionArguments, } from "../shared/embed-auth.js";
15
17
  import { isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
16
18
  import { countActionFailure, countCredentialState, } from "../tracking/failure-counters.js";
17
19
  import { redact, redactErrorStack } from "../tracking/redaction.js";
18
20
  import { notifyActionChange } from "./action-change.js";
19
- import { readBrowserSessionIdHeader, readBrowserTabIdHeader, readAnalyticsClientPlatformHeader, readSyntheticTrafficHeader, seedAgentRunOwnerContext, } from "./agent-run-context.js";
21
+ import { readBrowserSessionIdHeader, readBrowserTabIdHeader, readAnalyticsClientPlatformHeader, readSyntheticTrafficHeader, resolveAgentRunOrgId, seedAgentRunOwnerContext, } from "./agent-run-context.js";
20
22
  import { getConfiguredAppBasePath } from "./app-base-path.js";
21
23
  import { captureError } from "./capture-error.js";
22
24
  import { getAllowedCorsOrigin as resolveAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
23
- import { resolveEmbedSessionFromRequest, resolvedEmbedCapabilityScope, } from "./embed-session.js";
25
+ import { hasExplicitEmbedSessionCredential, isExpiredMcpDirectoryWidgetSessionRequest, resolveEmbedSessionFromRequest, resolvedEmbedCapabilityScope, } from "./embed-session.js";
24
26
  import { readBodyWithSizeLimit } from "./h3-helpers.js";
25
27
  import { getHttpRequestTelemetryId, registerHttpRequestTelemetryActionRoute, setHttpRequestTelemetryActionName, } from "./http-response-telemetry.js";
26
28
  import { consumeOneTimeJti } from "./identity-sso-store.js";
@@ -47,6 +49,7 @@ import { getVerifiedServiceIdentityFromEvent, getRequestIdentityAuthenticatedAtM
47
49
  const ROUTE_PREFIX = "/_agent-native/actions";
48
50
  const WEBMCP_ACTION_ROUTE_PREFIX = "/_agent-native/webmcp/actions";
49
51
  const MAX_MCP_DIRECTORY_WIDGET_SCHEMA_ARGUMENT_BYTES = 32 * 1024;
52
+ const MAX_MCP_DIRECTORY_WIDGET_WRITE_SCHEMA_ARGUMENT_BYTES = 1024 * 1024;
50
53
  const FRONTEND_MUTATION_METHODS = new Set(["POST", "PUT", "DELETE"]);
51
54
  const EMBED_ACTION_QUERY_PARAMS = new Set([
52
55
  EMBED_TARGET_QUERY_PARAM,
@@ -307,13 +310,14 @@ function allowsWebMcpCapabilityResource(authCapability, params) {
307
310
  }
308
311
  }
309
312
  async function resolveRequestAuthCapability(event) {
310
- try {
311
- return resolvedEmbedCapabilityScope(await resolveEmbedSessionFromRequest(event));
312
- }
313
- catch {
314
- // Invalid or unavailable embed auth must fail closed as no capability.
315
- return undefined;
313
+ const session = await resolveEmbedSessionFromRequest(event);
314
+ if (!session && hasExplicitEmbedSessionCredential(event)) {
315
+ if (await isExpiredMcpDirectoryWidgetSessionRequest(event)) {
316
+ setResponseHeader(event, MCP_DIRECTORY_WIDGET_SESSION_EXPIRED_HEADER, "1");
317
+ }
318
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
316
319
  }
320
+ return resolvedEmbedCapabilityScope(session);
317
321
  }
318
322
  function resolveRequestEmbedSession(event) {
319
323
  return resolveEmbedSessionFromRequest(event);
@@ -335,7 +339,9 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
335
339
  (Array.isArray(entry.capabilityScopes) &&
336
340
  entry.capabilityScopes.length) ||
337
341
  (!options?.caller &&
338
- options?.mcpDirectoryWidgetReadActionArguments?.[name] !== undefined)) {
342
+ (options?.mcpDirectoryWidgetReadActionArguments?.[name] !== undefined ||
343
+ options?.mcpDirectoryWidgetWriteActionArguments?.[name] !==
344
+ undefined))) {
339
345
  registerAuthPublicPaths([routePath], app);
340
346
  }
341
347
  if (!options?.caller &&
@@ -351,7 +357,7 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
351
357
  return handleOptionsRequest(event);
352
358
  }
353
359
  setResponseHeader(event, "Cache-Control", "no-store");
354
- setResponseHeader(event, "Access-Control-Expose-Headers", "X-Agent-Native-Client-Mismatch,X-Agent-Native-Build-Id,X-Agent-Native-Client-Compatibility,Retry-After");
360
+ setResponseHeader(event, "Access-Control-Expose-Headers", `X-Agent-Native-Client-Mismatch,X-Agent-Native-Build-Id,X-Agent-Native-Client-Compatibility,Retry-After,${MCP_DIRECTORY_WIDGET_SESSION_EXPIRED_HEADER}`);
355
361
  const isFrontendMutation = isFrontendActionRequest(event) &&
356
362
  FRONTEND_MUTATION_METHODS.has(method) &&
357
363
  FRONTEND_MUTATION_METHODS.has(effectiveMethod);
@@ -387,14 +393,23 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
387
393
  let userName;
388
394
  let authUserId;
389
395
  const authCapability = await resolveRequestAuthCapability(event);
390
- const directoryWidgetReadCapability = isMcpDirectoryWidgetReadCapabilityScope(authCapability);
391
- const embedSession = directoryWidgetReadCapability
396
+ const directoryWidgetWriteCapability = isMcpDirectoryWidgetWriteCapabilityScope(authCapability);
397
+ const directoryWidgetCapability = isMcpDirectoryWidgetCapabilityScope(authCapability);
398
+ const embedSession = directoryWidgetCapability
392
399
  ? await resolveRequestEmbedSession(event)
393
400
  : null;
401
+ const directoryWidgetWriteGrant = directoryWidgetWriteCapability && embedSession
402
+ ? getMcpDirectoryWidgetWriteCapabilityGrant(authCapability, {
403
+ appId: options?.mcpDirectoryWidgetAppId ?? options?.appId ?? "",
404
+ resourceUri: options?.mcpDirectoryWidgetResourceUri ?? "",
405
+ userEmail: embedSession.email,
406
+ orgId: embedSession.orgId,
407
+ })
408
+ : undefined;
394
409
  const directoryWidgetReadRequest = isFrontendActionRequest(event) ||
395
410
  (options?.caller === "webmcp" &&
396
411
  options.routePrefix === WEBMCP_ACTION_ROUTE_PREFIX);
397
- const directoryWidgetReadAllowed = directoryWidgetReadCapability &&
412
+ const directoryWidgetReadAllowed = directoryWidgetCapability &&
398
413
  embedSession !== null &&
399
414
  directoryWidgetReadRequest &&
400
415
  entry.http !== false &&
@@ -411,19 +426,34 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
411
426
  actionName: name,
412
427
  appId: options.mcpDirectoryWidgetAppId ?? options.appId,
413
428
  resourceUri: options.mcpDirectoryWidgetResourceUri,
429
+ userEmail: embedSession?.email,
430
+ orgId: embedSession?.orgId,
414
431
  allowedArgumentNames: options.mcpDirectoryWidgetReadActionArguments[name],
415
432
  requireArgumentMatch: false,
416
433
  });
434
+ const directoryWidgetWriteAllowed = directoryWidgetWriteCapability &&
435
+ embedSession !== null &&
436
+ isFrontendActionRequest(event) &&
437
+ entry.http !== false &&
438
+ entry.http?.method !== "GET" &&
439
+ entry.readOnly !== true &&
440
+ entry.requiresAuth !== false &&
441
+ options?.mcpDirectoryWidgetWriteActionArguments?.[name] !== undefined;
442
+ let directoryWidgetWriteResourceIds;
417
443
  const directoryWidgetReadRoute = options?.mcpDirectoryWidgetReadActionArguments?.[name] !==
418
444
  undefined &&
419
445
  options?.mcpDirectoryWidgetReadPublicActions?.includes(name) !== true;
420
- if (directoryWidgetReadCapability && !directoryWidgetReadAllowed) {
446
+ const directoryWidgetWriteRoute = options?.mcpDirectoryWidgetWriteActionArguments?.[name] !== undefined;
447
+ if (directoryWidgetCapability &&
448
+ !directoryWidgetReadAllowed &&
449
+ !directoryWidgetWriteAllowed) {
421
450
  setResponseStatus(event, 403);
422
451
  return {
423
452
  error: "This widget capability only permits its scoped data routes.",
424
453
  };
425
454
  }
426
- if (directoryWidgetReadAllowed && embedSession) {
455
+ if ((directoryWidgetReadAllowed || directoryWidgetWriteAllowed) &&
456
+ embedSession) {
427
457
  userEmail = embedSession.email;
428
458
  if (embedSession.orgId == null)
429
459
  markExplicitPersonalOrgScope(event);
@@ -446,9 +476,11 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
446
476
  let directoryWidgetReadAuthenticatedFallback = false;
447
477
  const capabilityAllowed = (options?.caller === "webmcp" || isFrontendActionRequest(event)) &&
448
478
  (allowsWebMcpCapability(entry, authCapability) ||
449
- directoryWidgetReadAllowed);
479
+ directoryWidgetReadAllowed ||
480
+ directoryWidgetWriteAllowed);
450
481
  if (options?.allowDelegatedCaller !== false &&
451
- !directoryWidgetReadAllowed) {
482
+ !directoryWidgetReadAllowed &&
483
+ !directoryWidgetWriteAllowed) {
452
484
  let caller;
453
485
  try {
454
486
  caller = options?.actionRouteAuth?.resolveCaller
@@ -512,9 +544,71 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
512
544
  }
513
545
  }
514
546
  }
547
+ if (directoryWidgetWriteRoute && !directoryWidgetWriteAllowed) {
548
+ let ownerContext;
549
+ if (resolvedCaller) {
550
+ if (resolvedCaller.anonymous) {
551
+ setResponseStatus(event, 401);
552
+ return { error: "Unauthorized" };
553
+ }
554
+ ownerContext = resolvedCaller;
555
+ }
556
+ else {
557
+ if (!options?.getOwnerContextFromEvent) {
558
+ setResponseStatus(event, 401);
559
+ return { error: "Unauthorized" };
560
+ }
561
+ try {
562
+ ownerContext = await options.getOwnerContextFromEvent(event);
563
+ }
564
+ catch (error) {
565
+ if (!isAuthResolutionFailure(error))
566
+ throw error;
567
+ setResponseStatus(event, 401);
568
+ return { error: "Unauthorized" };
569
+ }
570
+ if (ownerContext.anonymous) {
571
+ setResponseStatus(event, 401);
572
+ return { error: "Unauthorized" };
573
+ }
574
+ userEmail = ownerContext.owner;
575
+ userName = ownerContext.name;
576
+ authUserId = ownerContext.authUserId;
577
+ ownerContextResolved = true;
578
+ }
579
+ const workspaceOrgId = resolvedCaller
580
+ ? resolvedCaller.orgId === null
581
+ ? undefined
582
+ : (normalizeOrgId(resolvedCaller.orgId) ??
583
+ (resolvedCaller.owner && !resolvedCaller.anonymous
584
+ ? await storedActiveOrgId(resolvedCaller.owner)
585
+ : undefined))
586
+ : await resolveAgentRunOrgId({
587
+ event,
588
+ ownerContext,
589
+ resolveOrgId: options?.resolveOrgId,
590
+ });
591
+ const workspaceAppAccess = await checkWorkspaceAppAccessForRequest({
592
+ path: `/_agent-native/actions/${name}`,
593
+ method: getMethod(event),
594
+ email: ownerContext.owner,
595
+ orgId: workspaceOrgId,
596
+ });
597
+ if (workspaceAppAccess === WORKSPACE_APP_ACCESS_UNAVAILABLE) {
598
+ setResponseStatus(event, 503);
599
+ return { error: WORKSPACE_APP_ACCESS_UNAVAILABLE_MESSAGE };
600
+ }
601
+ if (workspaceAppAccess === false) {
602
+ setResponseStatus(event, 403);
603
+ return {
604
+ error: "You do not have access to this workspace app.",
605
+ };
606
+ }
607
+ }
515
608
  if (!resolvedCaller &&
516
609
  !ownerContextResolved &&
517
610
  !directoryWidgetReadAllowed &&
611
+ !directoryWidgetWriteAllowed &&
518
612
  options?.caller === "webmcp" &&
519
613
  options?.getOwnerContextFromEvent) {
520
614
  ownerContextResolved = true;
@@ -549,6 +643,7 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
549
643
  if (!resolvedCaller &&
550
644
  !ownerContextResolved &&
551
645
  !directoryWidgetReadAllowed &&
646
+ !directoryWidgetWriteAllowed &&
552
647
  options?.getOwnerFromEvent) {
553
648
  try {
554
649
  userEmail = await options.getOwnerFromEvent(event);
@@ -574,6 +669,7 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
574
669
  !resolvedCaller &&
575
670
  !directoryWidgetReadAuthenticatedFallback &&
576
671
  !directoryWidgetReadAllowed &&
672
+ !directoryWidgetWriteAllowed &&
577
673
  options?.getAuthUserIdFromEvent) {
578
674
  try {
579
675
  authUserId = await options.getAuthUserIdFromEvent(event);
@@ -593,7 +689,8 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
593
689
  // token caller's actions execute under. Non-adapter callers keep the
594
690
  // original resolveOrgId-only behavior.
595
691
  let orgId;
596
- if (directoryWidgetReadAllowed && embedSession) {
692
+ if ((directoryWidgetReadAllowed || directoryWidgetWriteAllowed) &&
693
+ embedSession) {
597
694
  orgId = normalizeOrgId(embedSession.orgId);
598
695
  }
599
696
  else if (resolvedCaller) {
@@ -746,6 +843,8 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
746
843
  appId: options.mcpDirectoryWidgetAppId ?? options.appId,
747
844
  resourceUri: options.mcpDirectoryWidgetResourceUri,
748
845
  args: params,
846
+ userEmail: embedSession?.email,
847
+ orgId: embedSession?.orgId,
749
848
  allowedArgumentNames: options.mcpDirectoryWidgetReadActionArguments?.[name],
750
849
  });
751
850
  if (!normalizedArgs) {
@@ -756,6 +855,42 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
756
855
  }
757
856
  params = normalizedArgs;
758
857
  }
858
+ if (directoryWidgetWriteAllowed) {
859
+ const normalizedArgs = normalizeMcpDirectoryWidgetWriteActionArguments(authCapability, {
860
+ actionName: name,
861
+ appId: options.mcpDirectoryWidgetAppId ?? options.appId,
862
+ resourceUri: options.mcpDirectoryWidgetResourceUri,
863
+ userEmail: embedSession?.email,
864
+ orgId: embedSession?.orgId,
865
+ args: params,
866
+ allowedArgumentNames: options.mcpDirectoryWidgetWriteActionArguments?.[name],
867
+ });
868
+ directoryWidgetWriteResourceIds =
869
+ getMcpDirectoryWidgetWriteCapabilityGrant(authCapability, {
870
+ appId: options.mcpDirectoryWidgetAppId ?? options.appId ?? "",
871
+ resourceUri: options.mcpDirectoryWidgetResourceUri ?? "",
872
+ userEmail: embedSession?.email ?? "",
873
+ orgId: embedSession?.orgId,
874
+ })?.resourceIds;
875
+ if (!normalizedArgs || !directoryWidgetWriteResourceIds) {
876
+ const expired = isExpiredMcpDirectoryWidgetWriteCapability(authCapability, {
877
+ appId: options.mcpDirectoryWidgetAppId ?? options.appId ?? "",
878
+ resourceUri: options.mcpDirectoryWidgetResourceUri ?? "",
879
+ userEmail: embedSession?.email ?? "",
880
+ orgId: embedSession?.orgId,
881
+ });
882
+ if (expired) {
883
+ setResponseHeader(event, MCP_DIRECTORY_WIDGET_SESSION_EXPIRED_HEADER, "1");
884
+ }
885
+ throw createError({
886
+ statusCode: expired ? 401 : 403,
887
+ statusMessage: expired
888
+ ? "Unauthorized"
889
+ : "This widget write capability is scoped to a different user, app resource, or action.",
890
+ });
891
+ }
892
+ params = normalizedArgs;
893
+ }
759
894
  if (capabilityAllowed &&
760
895
  !userEmail &&
761
896
  !allowsWebMcpCapabilityResource(authCapability, params)) {
@@ -766,12 +901,14 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
766
901
  }
767
902
  const caller = directoryWidgetReadAllowed
768
903
  ? "mcp-widget"
769
- : (options?.caller ??
770
- (resolvedCaller
771
- ? "a2a"
772
- : isFrontendActionRequest(event)
773
- ? "frontend"
774
- : "http"));
904
+ : directoryWidgetWriteAllowed
905
+ ? "mcp-widget-write"
906
+ : (options?.caller ??
907
+ (resolvedCaller
908
+ ? "a2a"
909
+ : isFrontendActionRequest(event)
910
+ ? "frontend"
911
+ : "http"));
775
912
  const runContext = {
776
913
  userEmail,
777
914
  orgId: orgId ?? null,
@@ -781,6 +918,18 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
781
918
  ...(directoryWidgetReadAllowed
782
919
  ? { mcpDirectoryWidgetReadOnly: true }
783
920
  : {}),
921
+ ...(directoryWidgetWriteGrant &&
922
+ (directoryWidgetWriteAllowed || directoryWidgetReadAllowed)
923
+ ? {
924
+ mcpDirectoryWidgetWrite: {
925
+ appId: options?.mcpDirectoryWidgetAppId ??
926
+ options?.appId ??
927
+ "",
928
+ resourceIds: directoryWidgetWriteGrant.resourceIds,
929
+ actionNames: directoryWidgetWriteGrant.actionNames,
930
+ },
931
+ }
932
+ : {}),
784
933
  ...(event.req?.signal ? { signal: event.req.signal } : {}),
785
934
  actionName: name,
786
935
  ...(resolvedCaller?.delegationJti
@@ -816,16 +965,23 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
816
965
  throw new ActionContractError(`"${name}" requires human approval for these arguments. WebMCP tool calls cannot grant that approval themselves — ask the user to confirm this action in chat, then call it there.`, { errorCode: "approval_required", statusCode: 409 });
817
966
  }
818
967
  }
819
- else if (directoryWidgetReadAllowed &&
820
- (options?.mcpDirectoryWidgetReadActionSchemaArguments?.[name]
821
- ?.length ?? 0) > 0) {
822
- const oversizedSchemaArgument = options?.mcpDirectoryWidgetReadActionSchemaArguments?.[name]?.find((argumentName) => {
968
+ else if ((directoryWidgetReadAllowed || directoryWidgetWriteAllowed) &&
969
+ ((directoryWidgetReadAllowed
970
+ ? options?.mcpDirectoryWidgetReadActionSchemaArguments?.[name]
971
+ : options?.mcpDirectoryWidgetWriteActionSchemaArguments?.[name])?.length ?? 0) > 0) {
972
+ const schemaArguments = directoryWidgetReadAllowed
973
+ ? options?.mcpDirectoryWidgetReadActionSchemaArguments?.[name]
974
+ : options?.mcpDirectoryWidgetWriteActionSchemaArguments?.[name];
975
+ const maxSchemaArgumentBytes = directoryWidgetWriteAllowed
976
+ ? MAX_MCP_DIRECTORY_WIDGET_WRITE_SCHEMA_ARGUMENT_BYTES
977
+ : MAX_MCP_DIRECTORY_WIDGET_SCHEMA_ARGUMENT_BYTES;
978
+ const oversizedSchemaArgument = schemaArguments?.find((argumentName) => {
823
979
  if (!Object.hasOwn(params, argumentName))
824
980
  return false;
825
981
  const serialized = JSON.stringify(params[argumentName]);
826
982
  return (typeof serialized !== "string" ||
827
983
  new TextEncoder().encode(serialized).byteLength >
828
- MAX_MCP_DIRECTORY_WIDGET_SCHEMA_ARGUMENT_BYTES);
984
+ maxSchemaArgumentBytes);
829
985
  });
830
986
  if (oversizedSchemaArgument) {
831
987
  throw new ActionContractError("MCP directory widget query arguments exceed the supported size.", {
@@ -836,7 +992,7 @@ function mountActionRoutesInternal(nitroApp, actions, options) {
836
992
  if (!entry.schema ||
837
993
  typeof entry.schema !== "object" ||
838
994
  !("~standard" in entry.schema)) {
839
- throw new Error(`MCP directory widget read action "${name}" requires an input schema.`);
995
+ throw new Error(`MCP directory widget action "${name}" requires an input schema.`);
840
996
  }
841
997
  params = await validateActionArgs(entry.schema, params, entry.tool.parameters, runContext);
842
998
  }
@@ -1,4 +1,4 @@
1
- import type { McpDirectoryWidgetTarget } from "../../mcp/build-server.js";
1
+ import type { MCPConfig, McpDirectoryWidgetTarget } from "../../mcp/build-server.js";
2
2
  import type { ExternalAgentPolicy } from "../../mcp/external-agent-policy.js";
3
3
  import type { McpDirectoryWidgetReadArgument } from "../../shared/embed-auth.js";
4
4
  export interface AgentChatMcpIcon {
@@ -16,7 +16,9 @@ export interface AgentChatMcpOptions {
16
16
  instructions?: string;
17
17
  widgets?: boolean;
18
18
  widgetTargets?: Record<string, (args: Record<string, unknown>, result: unknown) => McpDirectoryWidgetTarget | null>;
19
+ authorizeWidgetWrite?: NonNullable<MCPConfig["directoryProfile"]>["authorizeWidgetWrite"];
19
20
  widgetReadActionArguments?: Record<string, Record<string, McpDirectoryWidgetReadArgument>>;
21
+ widgetWriteActionArguments?: Record<string, Record<string, McpDirectoryWidgetReadArgument>>;
20
22
  /** Actions whose capability-backed `mcp-widget` execution is strictly read-only. */
21
23
  widgetReadOnlyActions?: readonly string[];
22
24
  /** Unlisted public reads that are available only through a scoped widget ticket. */
@@ -2330,7 +2330,7 @@ export function createAgentChatPlugin(options) {
2330
2330
  };
2331
2331
  if (mcpOptions.enabled) {
2332
2332
  // Mount MCP remote server — same action registry as A2A + agent chat
2333
- const { mountMCP, selectMcpDirectoryWidgetReadActions } = await import("../mcp/server.js");
2333
+ const { mountMCP, selectMcpDirectoryWidgetReadActions, selectMcpDirectoryWidgetWriteActions, } = await import("../mcp/server.js");
2334
2334
  mountMCP(nitroApp, {
2335
2335
  name: mcpServerName,
2336
2336
  title: mcpOptions.title,
@@ -2345,6 +2345,7 @@ export function createAgentChatPlugin(options) {
2345
2345
  actions: externalActions,
2346
2346
  productionActions: externalFullActions,
2347
2347
  widgetReadActions: selectMcpDirectoryWidgetReadActions(mcpOptions.directoryProfile, templateScriptsAll),
2348
+ widgetWriteActions: selectMcpDirectoryWidgetWriteActions(mcpOptions.directoryProfile, templateScriptsAll),
2348
2349
  ...(mcpOptions.catalog ? { catalogMode: mcpOptions.catalog } : {}),
2349
2350
  ...(mcpOptions.builtinCrossAppTools !== undefined
2350
2351
  ? { builtinCrossAppTools: mcpOptions.builtinCrossAppTools }
@@ -2605,6 +2606,29 @@ export function createAgentChatPlugin(options) {
2605
2606
  mcpDirectoryWidgetResourceUri: getMcpDirectoryWidgetResourceUri(options?.appId ?? mcpServerName),
2606
2607
  }
2607
2608
  : {};
2609
+ const mcpDirectoryWidgetWriteOptions = directoryProfile
2610
+ ? {
2611
+ mcpDirectoryWidgetWriteActionArguments: Object.fromEntries(Object.entries(directoryProfile.widgetWriteActionArguments ?? {})
2612
+ .filter(([name]) => httpActions[name] &&
2613
+ httpActions[name]?.http !== false &&
2614
+ httpActions[name]?.http?.method !== "GET" &&
2615
+ httpActions[name]?.requiresAuth !== false &&
2616
+ httpActions[name]?.readOnly !== true)
2617
+ .map(([name, args]) => [name, Object.keys(args)])),
2618
+ mcpDirectoryWidgetWriteActionSchemaArguments: Object.fromEntries(Object.entries(directoryProfile.widgetWriteActionArguments ?? {})
2619
+ .map(([name, args]) => [
2620
+ name,
2621
+ Object.entries(args)
2622
+ .filter(([, argument]) => typeof argument !== "string" &&
2623
+ (argument.type === "actionSchema" ||
2624
+ argument.type === "actionSchemaResourceBound"))
2625
+ .map(([argumentName]) => argumentName),
2626
+ ])
2627
+ .filter(([, argumentNames]) => argumentNames.length > 0)),
2628
+ mcpDirectoryWidgetAppId: options?.appId ?? mcpServerName,
2629
+ mcpDirectoryWidgetResourceUri: getMcpDirectoryWidgetResourceUri(options?.appId ?? mcpServerName),
2630
+ }
2631
+ : {};
2608
2632
  if (Object.keys(httpActions).length > 0) {
2609
2633
  if (options?.actionRoutePublicPaths?.length) {
2610
2634
  registerAuthPublicPaths(options.actionRoutePublicPaths, getH3App(nitroApp));
@@ -2619,6 +2643,7 @@ export function createAgentChatPlugin(options) {
2619
2643
  resolveOrgId: options?.resolveOrgId,
2620
2644
  actionRouteAuth: options?.actionRouteAuth,
2621
2645
  ...mcpDirectoryWidgetReadOptions,
2646
+ ...mcpDirectoryWidgetWriteOptions,
2622
2647
  });
2623
2648
  }
2624
2649
  // Dev-only loopback endpoint `pnpm action` forwards to so it doesn't
@@ -1,9 +1,9 @@
1
1
  export declare function createAgentEngineOllamaModelsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
2
  error: string;
3
- models?: undefined;
4
3
  ok?: undefined;
4
+ models?: undefined;
5
5
  } | {
6
+ error?: undefined;
6
7
  ok: boolean;
7
8
  models: string[];
8
- error?: undefined;
9
9
  }>>;
@@ -3,9 +3,9 @@ import { defineEventHandler, getMethod, getQuery, setResponseHeader, setResponse
3
3
  import { getAppConfig, resolveAppHomePath } from "../app-config/index.js";
4
4
  import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
5
5
  import { newOrgSelection, ORG_SELECTION_COOKIE, } from "../org/request-org-cache.js";
6
- import { isWorkspaceAppAccessAllowed, WORKSPACE_APP_ACCESS_UNAVAILABLE, WORKSPACE_APP_ACCESS_UNAVAILABLE_MESSAGE, } from "../org/workspace-app-access.js";
7
- import { resolveWorkspaceAccessAppId } from "../org/workspace-app-identity.js";
8
- import { EMBED_SESSION_COOKIE, EMBED_START_PATH, EMBED_TARGET_HEADER, isMcpDirectoryWidgetReadCapabilityScope, } from "../shared/embed-auth.js";
6
+ import { checkWorkspaceAppAccessForRequest } from "../org/workspace-app-access-request.js";
7
+ import { WORKSPACE_APP_ACCESS_UNAVAILABLE, WORKSPACE_APP_ACCESS_UNAVAILABLE_MESSAGE, } from "../org/workspace-app-access.js";
8
+ import { EMBED_SESSION_COOKIE, EMBED_START_PATH, EMBED_TARGET_HEADER, isMcpDirectoryWidgetCapabilityScope, } from "../shared/embed-auth.js";
9
9
  import { FIRST_RUN_ONBOARDING_COOKIE, FIRST_RUN_ONBOARDING_MAX_AGE, } from "../shared/first-run-onboarding.js";
10
10
  import { isGoogleProfileImageUrl } from "../shared/google-profile-image.js";
11
11
  import { EMBED_TRANSPLANT_HEADER, isMcpEmbedCorsOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, shouldAllowMcpEmbedCredentials, } from "../shared/mcp-embed-headers.js";
@@ -3038,20 +3038,11 @@ function createAuthGuardFn(app) {
3038
3038
  }
3039
3039
  const session = await getSession(event);
3040
3040
  if (session) {
3041
- const workspaceAppId = resolveWorkspaceAccessAppId();
3042
3041
  const method = getMethod(event);
3043
- const sharedWorkspaceAccessPath = p === "/_agent-native/org/me" ||
3044
- p === "/_agent-native/actions/list-workspace-apps" ||
3045
- (method === "GET" &&
3046
- p === "/_agent-native/actions/list-workspace-app-access") ||
3047
- (method === "POST" &&
3048
- p === "/_agent-native/actions/set-workspace-app-access");
3049
- // Keep org-owned repair controls reachable when this app is disabled;
3050
- // each action or handler still enforces its org membership and role.
3051
- if (workspaceAppId &&
3052
- !sharedWorkspaceAccessPath &&
3053
- (p.startsWith("/api/") || p.startsWith("/_agent-native/"))) {
3054
- const workspaceAppAccess = await isWorkspaceAppAccessAllowed(workspaceAppId, {
3042
+ if (p.startsWith("/api/") || p.startsWith("/_agent-native/")) {
3043
+ const workspaceAppAccess = await checkWorkspaceAppAccessForRequest({
3044
+ path: p,
3045
+ method,
3055
3046
  email: session.email,
3056
3047
  orgId: session.orgId,
3057
3048
  });
@@ -3059,7 +3050,7 @@ function createAuthGuardFn(app) {
3059
3050
  setResponseStatus(event, 503);
3060
3051
  return { error: WORKSPACE_APP_ACCESS_UNAVAILABLE_MESSAGE };
3061
3052
  }
3062
- if (!workspaceAppAccess) {
3053
+ if (workspaceAppAccess === false) {
3063
3054
  setResponseStatus(event, 403);
3064
3055
  return { error: "You do not have access to this workspace app." };
3065
3056
  }
@@ -3416,7 +3407,7 @@ async function resolveSessionUncached(event, options = {}) {
3416
3407
  const cookieOnlyDesktopCheck = isDesktopSessionCookieOnlyCheck(event);
3417
3408
  if (!options.ignoreEmbedSession) {
3418
3409
  const embedSession = await resolveEmbedSessionFromRequest(event);
3419
- if (isMcpDirectoryWidgetReadCapabilityApplicationStateRequest(event, embedSession?.scope)) {
3410
+ if (isMcpDirectoryWidgetCapabilityApplicationStateRequest(event, embedSession?.scope)) {
3420
3411
  return null;
3421
3412
  }
3422
3413
  if (embedSession && !isEmbedCapabilityScope(embedSession.scope)) {
@@ -3498,8 +3489,8 @@ async function resolveSessionUncached(event, options = {}) {
3498
3489
  return authDisabledSession;
3499
3490
  return null;
3500
3491
  }
3501
- function isMcpDirectoryWidgetReadCapabilityApplicationStateRequest(event, scope) {
3502
- if (!isMcpDirectoryWidgetReadCapabilityScope(scope))
3492
+ function isMcpDirectoryWidgetCapabilityApplicationStateRequest(event, scope) {
3493
+ if (!isMcpDirectoryWidgetCapabilityScope(scope))
3503
3494
  return false;
3504
3495
  const rawUrl = event.node?.req?.url ?? event.path ?? "/";
3505
3496
  const base = "http://agent-native.invalid";
@@ -1,6 +1,6 @@
1
1
  import { defineEventHandler, getHeader, getMethod, getQuery, setResponseHeader, } from "h3";
2
2
  import { withCollapsedAgentSidebarParam } from "../shared/agent-sidebar-url.js";
3
- import { EMBED_MODE_QUERY_PARAM, EMBED_START_PATH, EMBED_TOKEN_QUERY_PARAM, isMcpDirectoryWidgetReadCapabilityScope, MCP_APP_CHAT_BRIDGE_QUERY_PARAM, MCP_DIRECTORY_WIDGET_QUERY_PARAM, } from "../shared/embed-auth.js";
3
+ import { EMBED_MODE_QUERY_PARAM, EMBED_START_PATH, EMBED_TOKEN_QUERY_PARAM, isMcpDirectoryWidgetCapabilityScope, MCP_APP_CHAT_BRIDGE_QUERY_PARAM, MCP_DIRECTORY_WIDGET_QUERY_PARAM, } from "../shared/embed-auth.js";
4
4
  import { isMcpEmbedTransplantOrigin, MCP_EMBED_CORS_ALLOW_HEADERS, } from "../shared/mcp-embed-headers.js";
5
5
  import { getConfiguredAppBasePath } from "./app-base-path.js";
6
6
  import { readCorsAllowedOrigins } from "./cors-origins.js";
@@ -283,9 +283,11 @@ export function createEmbedStartRouteHandler(options = {}) {
283
283
  audienceHost: getForwardedRequestHostname(event),
284
284
  scope: consumed.scope,
285
285
  ...(consumed.ticketCreatedAtMs != null &&
286
- !isEmbedCapabilityScope(consumed.scope)
286
+ (!isEmbedCapabilityScope(consumed.scope) ||
287
+ isMcpDirectoryWidgetCapabilityScope(consumed.scope))
287
288
  ? { ticketCreatedAtMs: consumed.ticketCreatedAtMs }
288
289
  : {}),
290
+ ...(consumed.sessionId ? { sessionId: consumed.sessionId } : {}),
289
291
  ...(isEmbedCapabilityScope(consumed.scope)
290
292
  ? {
291
293
  ttlSeconds: Math.max(1, Math.floor((consumed.expiresAt - Date.now()) / 1000)),
@@ -298,7 +300,7 @@ export function createEmbedStartRouteHandler(options = {}) {
298
300
  // its document is a widget whether or not the start URL carried the flag.
299
301
  const chatBridgeActive = firstQueryValue(query[MCP_APP_CHAT_BRIDGE_QUERY_PARAM]) === "1" ||
300
302
  firstQueryValue(query[MCP_APP_CHAT_BRIDGE_QUERY_PARAM]) === "true" ||
301
- isMcpDirectoryWidgetReadCapabilityScope(consumed.scope);
303
+ isMcpDirectoryWidgetCapabilityScope(consumed.scope);
302
304
  const location = withConfiguredBasePath(withCollapsedAgentSidebarParam(appendEmbedParams(target, token, chatBridgeActive)));
303
305
  const transplant = wantsTransplantLocationResponse(event);
304
306
  if (consumeDiagnostic) {
@@ -7,12 +7,26 @@ export interface EmbedSessionTicketInput {
7
7
  targetPath: string;
8
8
  scope?: string | null;
9
9
  ttlSeconds?: number;
10
+ renewalExpiresAtMs?: number;
11
+ revocationAnchorCreatedAtMs?: number;
10
12
  }
11
13
  export interface EmbedSessionTicket {
12
14
  ticket: string;
13
15
  ticketHash: string;
14
16
  expiresAt: number;
15
17
  }
18
+ export interface McpDirectoryWidgetRenewalTicket {
19
+ sessionId: string;
20
+ ownerEmail: string;
21
+ orgId?: string;
22
+ targetPath: string;
23
+ scope: string;
24
+ createdAtMs: number;
25
+ expiresAtMs: number;
26
+ consumedAtMs: number | null;
27
+ renewalExpiresAtMs: number;
28
+ sessionActiveUntilMs: number | null;
29
+ }
16
30
  export type EmbedSessionTicketConsumeOutcome = "missing-ticket" | "not-found" | "already-consumed" | "expired" | "identity-mismatch" | "org-mismatch" | "consumption-race" | "invalid-row" | "revoked" | "consumed";
17
31
  export interface EmbedSessionTicketConsumeDiagnostic {
18
32
  outcome: EmbedSessionTicketConsumeOutcome;
@@ -38,6 +52,7 @@ export interface ConsumedEmbedSessionTicket {
38
52
  scope?: string;
39
53
  expiresAt: number;
40
54
  ticketCreatedAtMs: number;
55
+ sessionId?: string;
41
56
  }
42
57
  export interface EmbedSessionTokenClaims {
43
58
  kind: typeof TOKEN_KIND;
@@ -49,6 +64,7 @@ export interface EmbedSessionTokenClaims {
49
64
  iat: number;
50
65
  issuedAtMs?: number;
51
66
  ticketCreatedAtMs?: number;
67
+ sessionId?: string;
52
68
  exp: number;
53
69
  }
54
70
  export type VerifyEmbedSessionTokenResult = {
@@ -73,7 +89,16 @@ export declare function revokeEmbedSessionsForOwners(ownerEmails: string[], inTr
73
89
  export declare function requestMatchesEmbedTarget(event: H3Event, targetPath: string): boolean;
74
90
  export declare function normalizeEmbedTargetPath(raw: string | undefined | null, requestOrigin?: string): string | null;
75
91
  export declare function createEmbedSessionTicket(input: EmbedSessionTicketInput): Promise<EmbedSessionTicket>;
92
+ export declare function readMcpDirectoryWidgetRenewalTicket(ticket: string): Promise<McpDirectoryWidgetRenewalTicket | null>;
93
+ export declare function renewMcpDirectoryWidgetSession(input: {
94
+ sourceTicket: string;
95
+ ownerEmail: string;
96
+ orgId?: string | null;
97
+ expectedScope: string;
98
+ renewedScope: string;
99
+ }): Promise<number | null>;
76
100
  export declare function resolveEmbedSessionTokenForHost(token: string | undefined, hostname: string): Promise<EmbedSessionTokenClaims | null>;
101
+ export declare function isExpiredMcpDirectoryWidgetSessionRequest(event: H3Event): Promise<boolean>;
77
102
  export declare function resolveEmbedSessionCookieOwners(tokens: string[]): Promise<string[]>;
78
103
  export declare function consumeEmbedSessionTicket(ticket: string | undefined | null, options?: ConsumeEmbedSessionTicketOptions): Promise<ConsumedEmbedSessionTicket | null>;
79
104
  export declare function signEmbedSessionToken(input: {
@@ -83,11 +108,15 @@ export declare function signEmbedSessionToken(input: {
83
108
  audienceHost?: string;
84
109
  scope?: string | null;
85
110
  ticketCreatedAtMs?: number;
111
+ sessionId?: string;
86
112
  ttlSeconds?: number;
87
113
  }): string;
88
- export declare function verifyEmbedSessionToken(token: string | undefined | null): VerifyEmbedSessionTokenResult;
114
+ export declare function verifyEmbedSessionToken(token: string | undefined | null, options?: {
115
+ allowExpired?: boolean;
116
+ }): VerifyEmbedSessionTokenResult;
89
117
  export declare function setEmbedSessionCookie(event: H3Event, token: string): void;
90
118
  export declare function resolveEmbedSessionFromRequest(event: H3Event): Promise<ResolvedEmbedSession | null>;
119
+ export declare function hasExplicitEmbedSessionCredential(event: H3Event): boolean;
91
120
  export declare function requestHasEmbedAuthMarker(event: H3Event): boolean;
92
121
  export declare function isEmbedModeRequest(event: H3Event): boolean;
93
122
  export {};