@agent-native/core 0.203.2-nightly-20261007213109 → 0.204.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 (121) hide show
  1. package/dist/a2a/handlers.js +57 -1
  2. package/dist/a2a/server.js +37 -1
  3. package/dist/action.d.ts +1 -0
  4. package/dist/action.js +26 -3
  5. package/dist/agent/contain-principal.d.ts +9 -0
  6. package/dist/agent/contain-principal.js +53 -0
  7. package/dist/agent/production-agent.js +32 -2
  8. package/dist/agent/run-manager.js +14 -2
  9. package/dist/agent/run-store.js +66 -12
  10. package/dist/audit/actions/export-audit-events.d.ts +1 -1
  11. package/dist/audit/actions/export-audit-events.js +2 -2
  12. package/dist/audit/actions/export-audit-ocsf.d.ts +11 -0
  13. package/dist/audit/actions/export-audit-ocsf.js +183 -0
  14. package/dist/audit/actions/list-audit-events.d.ts +1 -1
  15. package/dist/audit/actions/list-audit-events.js +3 -3
  16. package/dist/audit/config.d.ts +1 -1
  17. package/dist/audit/config.js +5 -1
  18. package/dist/audit/index.d.ts +1 -0
  19. package/dist/audit/index.js +1 -0
  20. package/dist/audit/ocsf.d.ts +70 -0
  21. package/dist/audit/ocsf.js +228 -0
  22. package/dist/audit/record.js +1 -1
  23. package/dist/audit/store.js +9 -2
  24. package/dist/audit/types.d.ts +12 -1
  25. package/dist/cli/agent.js +2 -11
  26. package/dist/cli/connect.js +7 -2
  27. package/dist/client/use-db-sync.d.ts +13 -0
  28. package/dist/client/use-db-sync.js +22 -1
  29. package/dist/coding-tools/run-code.js +22 -1
  30. package/dist/coding-tools/sandbox/local-child-process-adapter.js +20 -0
  31. package/dist/collab/client.js +36 -3
  32. package/dist/collab/routes.d.ts +9 -4
  33. package/dist/collab/routes.js +15 -1
  34. package/dist/collab/struct-routes.d.ts +1 -1
  35. package/dist/deploy/build.js +15 -8
  36. package/dist/deploy/workspace-deploy.js +88 -13
  37. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  38. package/dist/file-upload/builder.js +18 -4
  39. package/dist/framework-tools.js +3 -0
  40. package/dist/identity/rekey.js +6 -0
  41. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  42. package/dist/localization/core-messages/supplemental/ar-SA.js +15 -0
  43. package/dist/localization/core-messages/supplemental/de-DE.js +15 -0
  44. package/dist/localization/core-messages/supplemental/en-US.js +15 -0
  45. package/dist/localization/core-messages/supplemental/es-ES.js +15 -0
  46. package/dist/localization/core-messages/supplemental/fr-FR.js +15 -0
  47. package/dist/localization/core-messages/supplemental/hi-IN.js +15 -0
  48. package/dist/localization/core-messages/supplemental/ja-JP.js +15 -0
  49. package/dist/localization/core-messages/supplemental/ko-KR.js +15 -0
  50. package/dist/localization/core-messages/supplemental/pt-BR.js +15 -0
  51. package/dist/localization/core-messages/supplemental/zh-CN.js +15 -0
  52. package/dist/localization/core-messages/supplemental/zh-TW.js +15 -0
  53. package/dist/localization/mcp-settings-messages.d.ts +15 -0
  54. package/dist/mcp/actions/create-org-service-token.d.ts +8 -0
  55. package/dist/mcp/actions/create-org-service-token.js +93 -4
  56. package/dist/mcp/actions/list-org-service-tokens.d.ts +2 -0
  57. package/dist/mcp/actions/list-org-service-tokens.js +36 -8
  58. package/dist/mcp/actions/service-principal-input.d.ts +48 -0
  59. package/dist/mcp/actions/service-principal-input.js +102 -0
  60. package/dist/mcp/actions/set-service-principal-lifecycle.d.ts +15 -0
  61. package/dist/mcp/actions/set-service-principal-lifecycle.js +150 -0
  62. package/dist/mcp/actions/set-service-principal-policy.d.ts +14 -0
  63. package/dist/mcp/actions/set-service-principal-policy.js +105 -0
  64. package/dist/mcp/build-server.js +117 -13
  65. package/dist/mcp/connect-route.js +116 -4
  66. package/dist/mcp/connect-store.d.ts +7 -0
  67. package/dist/mcp/connect-store.js +21 -14
  68. package/dist/mcp-client/oauth-routes.js +2 -1
  69. package/dist/notifications/routes.d.ts +3 -3
  70. package/dist/observability/routes.d.ts +7 -7
  71. package/dist/org/index.d.ts +2 -2
  72. package/dist/org/service-principal-guard.d.ts +64 -0
  73. package/dist/org/service-principal-guard.js +140 -0
  74. package/dist/org/service-principal-policy.d.ts +104 -0
  75. package/dist/org/service-principal-policy.js +272 -0
  76. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  77. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  78. package/dist/scripts/dev/index.js +2 -8
  79. package/dist/scripts/parse-args.d.ts +7 -0
  80. package/dist/scripts/parse-args.js +76 -0
  81. package/dist/scripts/utils.d.ts +1 -1
  82. package/dist/scripts/utils.js +1 -1
  83. package/dist/secrets/routes.d.ts +11 -11
  84. package/dist/server/action-discovery.js +16 -42
  85. package/dist/server/agent-chat/script-entries.js +3 -8
  86. package/dist/server/agent-chat-plugin.js +8 -42
  87. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  88. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  89. package/dist/server/auth.js +5 -2
  90. package/dist/server/core-routes-plugin.js +25 -6
  91. package/dist/server/google-oauth.js +8 -4
  92. package/dist/server/identity-sso.js +2 -1
  93. package/dist/server/index.d.ts +1 -0
  94. package/dist/server/index.js +1 -0
  95. package/dist/server/poll.d.ts +9 -0
  96. package/dist/server/poll.js +86 -7
  97. package/dist/server/query-echo-safe-redirect.d.ts +10 -0
  98. package/dist/server/query-echo-safe-redirect.js +102 -0
  99. package/dist/server/release-schema.js +4 -0
  100. package/dist/server/workspace-provider-oauth.js +2 -1
  101. package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
  102. package/dist/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
  103. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
  104. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
  105. package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
  106. package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
  107. package/dist/triggers/actions/manage-automation.d.ts +7 -7
  108. package/dist/vite/client.js +1 -0
  109. package/docs/content/audit-log.mdx +15 -0
  110. package/docs/content/external-agents-catalog.mdx +19 -0
  111. package/docs/content/http-api.mdx +21 -2
  112. package/docs/content/pr-visual-recap.mdx +3 -1
  113. package/docs/content/template-clips-embed.mdx +1 -1
  114. package/docs/content/turn-into-app.mdx +118 -0
  115. package/package.json +4 -4
  116. package/src/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
  117. package/src/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
  118. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
  119. package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
  120. package/src/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
  121. package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
@@ -1,5 +1,6 @@
1
1
  import { renameThread, resolveThreadAccess, setThreadArchived, setThreadPinned, } from "../../chat-threads/store.js";
2
2
  import { dbExecToolParameters } from "../../scripts/db/tool-schemas.js";
3
+ import { serializeCliArgs } from "../../scripts/parse-args.js";
3
4
  import { captureCliOutput } from "../cli-capture.js";
4
5
  import { getAmbientUserEmail, getRequestOrgId, getRequestRunContext, getRequestUserEmail, } from "../request-context.js";
5
6
  function wrapCliScript(tool, cliDefault, opts) {
@@ -7,18 +8,12 @@ function wrapCliScript(tool, cliDefault, opts) {
7
8
  tool,
8
9
  ...(opts?.readOnly ? { readOnly: true } : {}),
9
10
  run: async (args) => {
10
- const cliArgs = [];
11
- for (const [k, v] of Object.entries(args)) {
11
+ for (const k of Object.keys(args)) {
12
12
  if (opts?.allowedArgs && !opts.allowedArgs.includes(k)) {
13
13
  throw new Error(`Unknown argument: ${k}`);
14
14
  }
15
- const raw = v;
16
- const value = raw != null && typeof raw === "object"
17
- ? JSON.stringify(raw)
18
- : String(raw);
19
- cliArgs.push(`--${k}`, value);
20
15
  }
21
- return captureCliOutput(() => cliDefault(cliArgs));
16
+ return captureCliOutput(() => cliDefault(serializeCliArgs(args)));
22
17
  },
23
18
  };
24
19
  }
@@ -46,6 +46,7 @@ import { setProgressPreListHook } from "../progress/store.js";
46
46
  import { getSkillNameFromPath } from "../resources/metadata.js";
47
47
  import { resourceList, resourceListAccessible, resourceGet, ensurePersonalDefaults, isWorkspaceResourceOwner, SHARED_OWNER, WORKSPACE_OWNER, } from "../resources/store.js";
48
48
  import { normalizeDatabaseToolsMode } from "../scripts/db/tool-mode.js";
49
+ import { normalizeShellArgs, serializeCliArgs } from "../scripts/parse-args.js";
49
50
  import { getSetting, putSetting } from "../settings/store.js";
50
51
  import { retryContextFromRequest, } from "../shared/agent-chat-run-not-started.js";
51
52
  import { ANALYTICS_CLIENT_PLATFORM_BODY_FIELD, normalizeAnalyticsClientPlatform, } from "../shared/analytics-platform.js";
@@ -1043,54 +1044,19 @@ export function createAgentChatPlugin(options) {
1043
1044
  }
1044
1045
  const tokens = [];
1045
1046
  if (typeof input?.args === "string" && input.args.trim()) {
1046
- let current = "";
1047
- let inSingle = false;
1048
- let inDouble = false;
1049
- let escape = false;
1050
- for (let i = 0; i < input.args.length; i++) {
1051
- const char = input.args[i];
1052
- if (escape) {
1053
- current += char;
1054
- escape = false;
1055
- continue;
1056
- }
1057
- if (char === "\\") {
1058
- if (inSingle) {
1059
- current += char;
1060
- }
1061
- else {
1062
- escape = true;
1063
- }
1064
- continue;
1065
- }
1066
- if (char === "'" && !inDouble) {
1067
- inSingle = !inSingle;
1068
- continue;
1069
- }
1070
- if (char === '"' && !inSingle) {
1071
- inDouble = !inDouble;
1072
- continue;
1073
- }
1074
- if (/\s/.test(char) && !inSingle && !inDouble) {
1075
- if (current.length > 0) {
1076
- tokens.push(current);
1077
- current = "";
1078
- }
1079
- continue;
1080
- }
1081
- current += char;
1082
- }
1083
- if (current.length > 0) {
1084
- tokens.push(current);
1085
- }
1047
+ tokens.push(...normalizeShellArgs(input.args, {
1048
+ backslashEscapes: true,
1049
+ splitAllWhitespace: true,
1050
+ }));
1086
1051
  }
1087
1052
  else if (input && typeof input === "object") {
1053
+ const actionArgs = {};
1088
1054
  for (const [k, v] of Object.entries(input)) {
1089
1055
  if (k === "args" || v === undefined || v === null)
1090
1056
  continue;
1091
- const strVal = typeof v === "object" ? JSON.stringify(v) : String(v);
1092
- tokens.push(`--${k}`, strVal);
1057
+ actionArgs[k] = v;
1093
1058
  }
1059
+ tokens.push(...serializeCliArgs(actionArgs));
1094
1060
  }
1095
1061
  const BLOCKED_OPERATORS = new Set([
1096
1062
  ";",
@@ -9,16 +9,16 @@ export declare const AGENT_ENGINE_API_KEY_ACTION_NAME = "agent-engine-api-key";
9
9
  */
10
10
  export declare function createAgentEngineDisconnectHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
11
11
  error: string;
12
- ok?: undefined;
13
12
  scope?: undefined;
13
+ ok?: undefined;
14
14
  } | {
15
15
  ok: boolean;
16
16
  error: any;
17
17
  scope?: undefined;
18
18
  } | {
19
- error?: undefined;
20
19
  ok: boolean;
21
20
  scope: "org" | "user";
21
+ error?: undefined;
22
22
  }>>;
23
23
  export interface DefaultModelSelectionRequest {
24
24
  engine: string;
@@ -1,6 +1,6 @@
1
1
  export declare function createAgentEngineOllamaModelsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
- ok?: undefined;
3
2
  error: string;
3
+ ok?: undefined;
4
4
  models?: undefined;
5
5
  } | {
6
6
  error?: undefined;
@@ -85,6 +85,7 @@ import * as loopback from "./loopback.js";
85
85
  import { encodeMagicLinkSignupAttribution, MAGIC_LINK_ATTRIBUTION_PARAM, } from "./magic-link-attribution.js";
86
86
  import { safeOAuthReturnUrl } from "./oauth-return-url.js";
87
87
  import { getDeploySettingsRequiredHtml, getOnboardingHtml, getResetPasswordHtml, } from "./onboarding-html.js";
88
+ import { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
88
89
  import { getRequestContext, markRequestIdentityAuthenticatedAtMs, hasContinuationLocalRequestContext, hasExplicitPersonalOrgScope, markExplicitPersonalOrgScope, runWithRequestContext, } from "./request-context.js";
89
90
  import { captureAuthError } from "./sentry.js";
90
91
  import { isTestIdentity } from "./test-identity.js";
@@ -4901,7 +4902,9 @@ async function mountBetterAuthRoutes(app, options) {
4901
4902
  response.status < 300) {
4902
4903
  setFirstRunOnboardingCookie(event);
4903
4904
  }
4904
- return response;
4905
+ return isResponse
4906
+ ? queryEchoSafeRedirect(event, response)
4907
+ : response;
4905
4908
  }));
4906
4909
  app.use("/_agent-native/auth/login", defineEventHandler(async (event) => {
4907
4910
  if (getMethod(event) !== "POST") {
@@ -4968,7 +4971,7 @@ async function mountBetterAuthRoutes(app, options) {
4968
4971
  ? query.return[0]
4969
4972
  : query.return;
4970
4973
  setFirstRunOnboardingCookie(event);
4971
- return redirectWithStagedCookies(event, safeReturnPath(rawReturn), 302);
4974
+ return queryEchoSafeRedirect(event, redirectWithStagedCookies(event, safeReturnPath(rawReturn), 302));
4972
4975
  }));
4973
4976
  app.use("/_agent-native/auth/magic-link", defineEventHandler(async (event) => {
4974
4977
  if (getMethod(event) === "POST" &&
@@ -4152,12 +4152,31 @@ export function createCoreRoutesPlugin(options = {}) {
4152
4152
  return { error: "Unauthorized" };
4153
4153
  }
4154
4154
  const userEmail = session.email;
4155
- const result = await runWithRequestContext({ userEmail, orgId: session.orgId }, () => uploadFile({
4156
- data: filePart.data,
4157
- filename: filePart.filename,
4158
- mimeType: filePart.type,
4159
- ownerEmail: userEmail,
4160
- }));
4155
+ let result;
4156
+ try {
4157
+ result = await runWithRequestContext({ userEmail, orgId: session.orgId }, () => uploadFile({
4158
+ data: filePart.data,
4159
+ filename: filePart.filename,
4160
+ mimeType: filePart.type,
4161
+ ownerEmail: userEmail,
4162
+ }));
4163
+ }
4164
+ catch (error) {
4165
+ // A thrown provider error (e.g. an upstream API rejecting the
4166
+ // request) carries its own `status`/`statusCode`, which h3 would
4167
+ // otherwise surface verbatim to the client — indistinguishable
4168
+ // from this route's own deliberate 4xx responses above and
4169
+ // useless for the composer's generic "could not upload" copy.
4170
+ // Normalize to one clear failure instead of leaking whatever
4171
+ // status the active provider happened to respond with. Use 503
4172
+ // (not 502/504) since gateway statuses get rewritten by the CDN
4173
+ // layer anyway — see cdnSafeOriginStatus.
4174
+ console.error("[file-upload] provider upload failed", error);
4175
+ setResponseStatus(event, 503);
4176
+ return {
4177
+ error: "The configured storage provider could not upload this file. Try again or check Settings → File uploads.",
4178
+ };
4179
+ }
4161
4180
  if (result) {
4162
4181
  setResponseStatus(event, 201);
4163
4182
  return result;
@@ -14,6 +14,7 @@ import { setIdentityGoogleAuthCookie } from "./identity-auth-provider.js";
14
14
  import { isNetlifyDeployPermalinkGoogleOAuthClientOrigin, isNetlifyDeployPermalinkGoogleOAuthClientRequest, } from "./identity-sso-store.js";
15
15
  import { appendSessionToOAuthReturnUrl } from "./oauth-return-url.js";
16
16
  import { EXPLICIT_PUBLIC_ORIGIN_ENV_KEYS, firstOriginFromEnv, getConfiguredOriginAllowlist, isLoopbackHost, normalizeOrigin, WORKSPACE_GATEWAY_ORIGIN_ENV_KEYS, } from "./origin-allowlist.js";
17
+ import { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
17
18
  import { isWorkspaceOAuthCallbackRelayEnabled } from "./workspace-oauth.js";
18
19
  function safeReturnPath(raw) {
19
20
  return normalizeAppPath(raw) ?? "/";
@@ -669,9 +670,6 @@ export function oauthCallbackResponse(event, email, opts) {
669
670
  </script></body></html>`);
670
671
  }
671
672
  const location = appendSessionToOAuthReturnUrl(opts.returnUrl, opts.sessionToken);
672
- setResponseStatus(event, 302);
673
- setResponseHeader(event, "Location", location);
674
- setResponseHeader(event, "Referrer-Policy", "no-referrer");
675
673
  const headers = new Headers({
676
674
  Location: location,
677
675
  "Referrer-Policy": "no-referrer",
@@ -679,7 +677,13 @@ export function oauthCallbackResponse(event, email, opts) {
679
677
  for (const cookie of event.res?.headers?.getSetCookie?.() ?? []) {
680
678
  headers.append("set-cookie", cookie);
681
679
  }
682
- return new Response(null, { status: 302, headers });
680
+ const response = queryEchoSafeRedirect(event, new Response(null, { status: 302, headers }));
681
+ if (response.status === 302) {
682
+ setResponseStatus(event, 302);
683
+ setResponseHeader(event, "Location", location);
684
+ }
685
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
686
+ return response;
683
687
  }
684
688
  export function oauthErrorPage(message, status = 400) {
685
689
  const safe = escapeHtml(message);
@@ -16,6 +16,7 @@ import { createOAuthSession, getAppUrl, getOrigin } from "./google-oauth.js";
16
16
  import { hasIdentityGoogleAuthCookie } from "./identity-auth-provider.js";
17
17
  import { IDENTITY_SSO_PROVIDER_ID } from "./identity-sso-provider.js";
18
18
  import { consumeSsoState, createSsoState, CANONICAL_IDENTITY_SSO_HUB_URL, NETLIFY_PREVIEW_IDENTITY_SSO_HUB_URL, getIdentityHubUrl, identitySsoLoginButtonHtml, isCanonicalIdentitySsoClientRequest, isDesktopSsoUserAgent, isIdentitySsoExplicitlyEnabled, isIdentitySsoEnabled, isNetlifyDeployPermalinkIdentitySsoClientRequest, isJtiReplayed, SSO_STATE_TTL_MS, } from "./identity-sso-store.js";
19
+ import { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
19
20
  import { getRequestContext, hasContinuationLocalRequestContext, runWithRequestContext, } from "./request-context.js";
20
21
  export { getIdentityHubUrl, identitySsoLoginButtonHtml, isIdentitySsoEnabled };
21
22
  export { IDENTITY_SSO_PROVIDER_ID };
@@ -63,7 +64,7 @@ function redirect(event, location) {
63
64
  const staged = event.res?.headers?.getSetCookie?.() ?? [];
64
65
  for (const cookie of staged)
65
66
  headers.append("set-cookie", cookie);
66
- return new Response("", { status: 302, headers });
67
+ return queryEchoSafeRedirect(event, new Response("", { status: 302, headers }));
67
68
  }
68
69
  function errorPage(message, loginPath) {
69
70
  const safe = message
@@ -86,6 +86,7 @@ export { createOnboardingPlugin, defaultOnboardingPlugin, } from "../onboarding/
86
86
  export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, uploadFile, builderFileUploadProvider, type FileUploadInput, type FileUploadProvider, type FileUploadResult, } from "../file-upload/index.js";
87
87
  export { createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, slackAdapter, telegramAdapter, whatsappAdapter, discordAdapter, microsoftTeamsAdapter, emailAdapter, assertPlatformCapability, type PlatformAdapter, type IncomingMessage, type OutgoingMessage, type PlatformAdapterCapabilities, type ImmediateWebhookResponse, type IntegrationStatus, type IntegrationsPluginOptions, type IntegrationExecutionContext, BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, type BuiltInChannelId, type ChannelCapabilities, type IntegrationAvailability, type IntegrationCatalogEntry, type IntegrationCategory, type IntegrationCredentialRequirement, type IntegrationIconKey, type IntegrationSupportMaturity, } from "../integrations/index.js";
88
88
  export { isElectron, isMobile, getOrigin, getAppBasePath, getAppUrl, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, encodeOAuthState, decodeOAuthState, encodeNetlifyPreviewGoogleOAuthRelayState, decodeNetlifyPreviewGoogleOAuthRelayState, wrapNetlifyPreviewGoogleOAuthState, getNetlifyPreviewGoogleOAuthCallbackUrl, isNetlifyPreviewGoogleOAuthCallbackUrl, isNetlifyPreviewGoogleOAuthRelayState, AGENT_NATIVE_GOOGLE_OAUTH_RELAY_SECRET_ENV, NETLIFY_PREVIEW_GOOGLE_OAUTH_CALLBACK_URL, logOAuthStateDecodeFailure, resolveOAuthOwner, createOAuthSession, oauthCallbackResponse, oauthErrorPage, oauthDesktopExchangePage, type OAuthStatePayload, type OAuthStateDecodeFailureReason, type DecodeOAuthStateResult, type OAuthOwnerResult, type OAuthSessionResult, } from "./google-oauth.js";
89
+ export { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
89
90
  export { buildWorkspaceProviderAuthorizationUrl, createWorkspaceProviderOAuthHandler, exchangeWorkspaceProviderOAuthCode, handleWorkspaceProviderOAuthCallback, handleWorkspaceProviderOAuthStart, hasWorkspaceProviderOAuthCredentials, isGoogleWorkspaceOAuthProvider, isWorkspaceProviderOAuthFlowValid, mergeWorkspaceOAuthValues, resolveWorkspaceProviderIdentity, workspaceProviderOAuthPath, type GenericWorkspaceOAuthProvider, type WorkspaceProviderOAuthFlow, } from "./workspace-provider-oauth.js";
90
91
  export { CredentialStoreUnavailableError, FeatureNotConfiguredError, hasBuilderPrivateKey, isBuilderEnvManaged, getBuilderProxyOrigin, getBuilderImageGenerationBaseUrl, getBuilderEmbeddingsBaseUrl, getBuilderVideoGenerationBaseUrl, getBuilderWebSearchBaseUrl, getBuilderAuthHeader, resolveBuilderPrivateKey, resolveBuilderAuthHeader, resolveHasBuilderPrivateKey, resolveHasCompleteBuilderConnection, resolveBuilderCredentials, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveBuilderGatewayAuth, resolveBuilderGatewayCredentials, resolveHasBuilderGatewayCredential, resolveBuilderCredentialSource, resolveBuilderCredential, readDeployCredentialEnv, resolveVercelDeploymentProtectionHeaders, writeBuilderCredentials, deleteBuilderCredentials, resolveSecret, resolveSecretDetailed, prefetchSecrets, BuilderCredentialLookupError, type BuilderCredentialsDetailed, type ResolvedSecretDetail, } from "./credential-provider.js";
91
92
  export { GEMINI_API_KEY, LEGACY_GEMINI_API_KEY, canonicalSecretKey, readGeminiDeployCredentialEnv, resolveGeminiApiKey, resolveGeminiApiKeyDetailed, resolveSecretWithAliases, resolveSecretWithAliasesDetailed, secretKeyNames, type ResolvedAliasedSecret, } from "./secret-key-aliases.js";
@@ -81,6 +81,7 @@ export { createOnboardingPlugin, defaultOnboardingPlugin, } from "../onboarding/
81
81
  export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploadProviders, getActiveFileUploadProvider, getActiveFileUploadProviderForRequest, uploadFile, builderFileUploadProvider, } from "../file-upload/index.js";
82
82
  export { createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, slackAdapter, telegramAdapter, whatsappAdapter, discordAdapter, microsoftTeamsAdapter, emailAdapter, assertPlatformCapability, BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, } from "../integrations/index.js";
83
83
  export { isElectron, isMobile, getOrigin, getAppBasePath, getAppUrl, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, encodeOAuthState, decodeOAuthState, encodeNetlifyPreviewGoogleOAuthRelayState, decodeNetlifyPreviewGoogleOAuthRelayState, wrapNetlifyPreviewGoogleOAuthState, getNetlifyPreviewGoogleOAuthCallbackUrl, isNetlifyPreviewGoogleOAuthCallbackUrl, isNetlifyPreviewGoogleOAuthRelayState, AGENT_NATIVE_GOOGLE_OAUTH_RELAY_SECRET_ENV, NETLIFY_PREVIEW_GOOGLE_OAUTH_CALLBACK_URL, logOAuthStateDecodeFailure, resolveOAuthOwner, createOAuthSession, oauthCallbackResponse, oauthErrorPage, oauthDesktopExchangePage, } from "./google-oauth.js";
84
+ export { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
84
85
  export { buildWorkspaceProviderAuthorizationUrl, createWorkspaceProviderOAuthHandler, exchangeWorkspaceProviderOAuthCode, handleWorkspaceProviderOAuthCallback, handleWorkspaceProviderOAuthStart, hasWorkspaceProviderOAuthCredentials, isGoogleWorkspaceOAuthProvider, isWorkspaceProviderOAuthFlowValid, mergeWorkspaceOAuthValues, resolveWorkspaceProviderIdentity, workspaceProviderOAuthPath, } from "./workspace-provider-oauth.js";
85
86
  export { CredentialStoreUnavailableError, FeatureNotConfiguredError, hasBuilderPrivateKey, isBuilderEnvManaged, getBuilderProxyOrigin, getBuilderImageGenerationBaseUrl, getBuilderEmbeddingsBaseUrl, getBuilderVideoGenerationBaseUrl, getBuilderWebSearchBaseUrl, getBuilderAuthHeader, resolveBuilderPrivateKey, resolveBuilderAuthHeader, resolveHasBuilderPrivateKey, resolveHasCompleteBuilderConnection, resolveBuilderCredentials, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentialsDetailed, resolveBuilderGatewayAuth, resolveBuilderGatewayCredentials, resolveHasBuilderGatewayCredential, resolveBuilderCredentialSource, resolveBuilderCredential, readDeployCredentialEnv, resolveVercelDeploymentProtectionHeaders, writeBuilderCredentials, deleteBuilderCredentials, resolveSecret, resolveSecretDetailed, prefetchSecrets, BuilderCredentialLookupError, } from "./credential-provider.js";
86
87
  export { GEMINI_API_KEY, LEGACY_GEMINI_API_KEY, canonicalSecretKey, readGeminiDeployCredentialEnv, resolveGeminiApiKey, resolveGeminiApiKeyDetailed, resolveSecretWithAliases, resolveSecretWithAliasesDetailed, secretKeyNames, } from "./secret-key-aliases.js";
@@ -86,6 +86,10 @@ export declare class AppSyncState {
86
86
  private accessCacheTtl;
87
87
  private durableEventId;
88
88
  getVersion(): number;
89
+ getPollBaseline(useDurableEvents: boolean): Promise<{
90
+ version: number;
91
+ cursor: string;
92
+ }>;
89
93
  getPollEmitter(): EventEmitter;
90
94
  wireLocalEmitters(): void;
91
95
  ensureSyncEventsTable(): Promise<boolean>;
@@ -154,6 +158,7 @@ export declare class AppSyncState {
154
158
  getChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, cursor?: SyncCursor): ChangeReadResult;
155
159
  getDurableChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, cursor?: SyncCursor): Promise<ChangeReadResult>;
156
160
  getCombinedChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, useDurableEvents: boolean, cursor?: SyncCursor): Promise<ChangeReadResult>;
161
+ private readCombinedChangesSinceForUser;
157
162
  seedVersionFromDb(): Promise<void>;
158
163
  checkExternalDbChanges(options: {
159
164
  durableEvents: boolean;
@@ -164,6 +169,10 @@ declare function hostedRealtimeTransportEnabled(): boolean;
164
169
  export declare const __hostedRealtimeTransportEnabledForTests: typeof hostedRealtimeTransportEnabled;
165
170
  export declare function getDefaultAppSyncState(): AppSyncState;
166
171
  export declare function getVersion(): number;
172
+ export declare function getCurrentPollBaseline(state?: AppSyncState): Promise<{
173
+ version: number;
174
+ cursor: string;
175
+ }>;
167
176
  export declare function getPollEmitter(): EventEmitter;
168
177
  export declare function invalidateCollabAccessCache(resourceType: string, resourceId: string): void;
169
178
  export declare function __resetCollabAccessCacheForTests(): void;
@@ -43,6 +43,7 @@ export const POLL_CHANGE_EVENT = "poll-change";
43
43
  const ACCESS_CACHE_TTL_MS = 30_000;
44
44
  const ACCESS_CACHE_DENY_TTL_MS = 5_000;
45
45
  const ACCESS_CACHE_MAX = 500;
46
+ const ACCESS_CHECK_WAIT_MS = 1_000;
46
47
  const SCREEN_REFRESH_KEY = "__screen_refresh__";
47
48
  const SCREEN_REFRESH_QUERY_LIMIT = 256;
48
49
  const SEED_SYNC_VERSION_SQL = `
@@ -278,7 +279,7 @@ export class AppSyncState {
278
279
  screenRefreshHasMore = false;
279
280
  localEmittersWired = false;
280
281
  accessCache = new Map();
281
- accessInFlight = new Set();
282
+ accessInFlight = new Map();
282
283
  accessInvalidationEpoch = new Map();
283
284
  accessAllowTtlMs;
284
285
  constructor(options = {}) {
@@ -311,6 +312,40 @@ export class AppSyncState {
311
312
  getVersion() {
312
313
  return this.version;
313
314
  }
315
+ async getPollBaseline(useDurableEvents) {
316
+ let cursor = { version: this.version, id: "" };
317
+ if (compareSyncCursors(this.latestCursor, cursor) > 0) {
318
+ cursor = this.latestCursor;
319
+ }
320
+ if (useDurableEvents && !syncEventsDisabled()) {
321
+ try {
322
+ const result = await this.getDb().execute("SELECT version, id FROM sync_events WHERE version = (SELECT MAX(version) FROM sync_events) ORDER BY id DESC LIMIT 1");
323
+ const row = result.rows[0];
324
+ if (row) {
325
+ const version = Number(row.version);
326
+ if (!Number.isSafeInteger(version) ||
327
+ version < 0 ||
328
+ typeof row.id !== "string") {
329
+ throw new Error("Durable sync event cursor is invalid");
330
+ }
331
+ const durableCursor = { version, id: row.id };
332
+ if (compareSyncCursors(durableCursor, cursor) > 0) {
333
+ cursor = durableCursor;
334
+ }
335
+ }
336
+ }
337
+ catch (error) {
338
+ if (isMissingRelationError(error)) {
339
+ throw new SyncEventsTableUnavailableError();
340
+ }
341
+ throw error;
342
+ }
343
+ }
344
+ return {
345
+ version: Math.max(this.version, cursor.version),
346
+ cursor: encodeSyncCursor(cursor),
347
+ };
348
+ }
314
349
  getPollEmitter() {
315
350
  return this.pollEmitter;
316
351
  }
@@ -551,7 +586,7 @@ export class AppSyncState {
551
586
  if (key.endsWith(suffix))
552
587
  this.accessCache.delete(key);
553
588
  }
554
- for (const key of Array.from(this.accessInFlight)) {
589
+ for (const key of Array.from(this.accessInFlight.keys())) {
555
590
  if (key.endsWith(suffix))
556
591
  this.accessInFlight.delete(key);
557
592
  }
@@ -572,10 +607,10 @@ export class AppSyncState {
572
607
  scheduleAccessCheck(key, resourceType, resourceId, userEmail, orgId) {
573
608
  if (this.accessInFlight.has(key))
574
609
  return;
575
- this.accessInFlight.add(key);
576
610
  const resourceKey = accessResourceKey(resourceType, resourceId);
577
611
  const epoch = this.accessInvalidationEpoch.get(resourceKey) ?? 0;
578
- void (async () => {
612
+ let settled = false;
613
+ const check = (async () => {
579
614
  try {
580
615
  const access = await this.resolveAccessFn(resourceType, resourceId, {
581
616
  userEmail,
@@ -593,9 +628,12 @@ export class AppSyncState {
593
628
  this.setAccessCache(key, false, Date.now());
594
629
  }
595
630
  finally {
631
+ settled = true;
596
632
  this.accessInFlight.delete(key);
597
633
  }
598
634
  })();
635
+ if (!settled)
636
+ this.accessInFlight.set(key, check);
599
637
  }
600
638
  __resetAccessCacheForTests() {
601
639
  this.accessCache.clear();
@@ -896,7 +934,7 @@ export class AppSyncState {
896
934
  return { version: this.version, events: [] };
897
935
  }
898
936
  const readCursor = cursor ?? { version: since, id: "" };
899
- if (readCursor.version <= 0 && readCursor.id === "") {
937
+ if (!cursor && readCursor.version <= 0 && readCursor.id === "") {
900
938
  try {
901
939
  const result = await this.getDb().execute("SELECT MAX(version) as max_version FROM sync_events");
902
940
  return {
@@ -1044,6 +1082,23 @@ export class AppSyncState {
1044
1082
  }
1045
1083
  }
1046
1084
  async getCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor) {
1085
+ const result = await this.readCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor);
1086
+ if (!result.cursorLimited || this.accessInFlight.size === 0)
1087
+ return result;
1088
+ // A read stopped at an event whose access check had not finished. The
1089
+ // check is already running, so waiting for it here (bounded) delivers the
1090
+ // event now instead of one poll interval later.
1091
+ let timer;
1092
+ await Promise.race([
1093
+ Promise.allSettled(this.accessInFlight.values()),
1094
+ new Promise((resolve) => {
1095
+ timer = setTimeout(resolve, ACCESS_CHECK_WAIT_MS);
1096
+ }),
1097
+ ]);
1098
+ clearTimeout(timer);
1099
+ return this.readCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor);
1100
+ }
1101
+ async readCombinedChangesSinceForUser(since, userEmail, orgId, useDurableEvents, cursor) {
1047
1102
  const memory = this.getChangesSinceForUser(since, userEmail, orgId, cursor);
1048
1103
  if (!useDurableEvents)
1049
1104
  return memory;
@@ -1066,8 +1121,7 @@ export class AppSyncState {
1066
1121
  if (cursor) {
1067
1122
  const limitedCursors = [memory, durable]
1068
1123
  .filter((result) => result.cursorLimited)
1069
- .map((result) => decodeSyncCursor(result.cursor))
1070
- .filter((value) => !!value);
1124
+ .map((result) => decodeSyncCursor(result.cursor) ?? cursor);
1071
1125
  if (limitedCursors.length > 0) {
1072
1126
  const boundary = limitedCursors.reduce((minimum, value) => compareSyncCursors(value, minimum) < 0 ? value : minimum);
1073
1127
  return {
@@ -1103,6 +1157,7 @@ export class AppSyncState {
1103
1157
  events: limitedVersions.length > 0
1104
1158
  ? events.filter((event) => event.version <= Math.min(...limitedVersions))
1105
1159
  : events,
1160
+ ...(limitedVersions.length > 0 ? { cursorLimited: true } : {}),
1106
1161
  };
1107
1162
  }
1108
1163
  async seedVersionFromDb() {
@@ -1356,6 +1411,30 @@ export function getDefaultAppSyncState() {
1356
1411
  export function getVersion() {
1357
1412
  return getDefaultAppSyncState().getVersion();
1358
1413
  }
1414
+ export async function getCurrentPollBaseline(state = getDefaultAppSyncState()) {
1415
+ if (syncEventsDisabled()) {
1416
+ await state.seedVersionFromDb();
1417
+ await state.checkExternalDbChanges({ durableEvents: false });
1418
+ return state.getPollBaseline(false);
1419
+ }
1420
+ const releaseOwnedServerlessPoll = isProductionServerlessFunctionRuntime() && appMigratesAtRelease();
1421
+ try {
1422
+ if (!releaseOwnedServerlessPoll) {
1423
+ await state.seedVersionFromDb();
1424
+ await state.checkExternalDbChanges({ durableEvents: true });
1425
+ }
1426
+ return await state.getPollBaseline(true);
1427
+ }
1428
+ catch (error) {
1429
+ if (!(error instanceof SyncEventsTableUnavailableError))
1430
+ throw error;
1431
+ if (releaseOwnedServerlessPoll)
1432
+ throw error;
1433
+ await state.seedVersionFromDb();
1434
+ await state.checkExternalDbChanges({ durableEvents: false });
1435
+ return state.getPollBaseline(false);
1436
+ }
1437
+ }
1359
1438
  export function getPollEmitter() {
1360
1439
  return getDefaultAppSyncState().getPollEmitter();
1361
1440
  }
@@ -0,0 +1,10 @@
1
+ import type { H3Event } from "h3";
2
+ /**
3
+ * Netlify's proxy copies the request's query string onto any redirect whose
4
+ * Location has none, so a bare 302 out of a sign-in callback lands the browser
5
+ * on a page still carrying the one-time `token`, OAuth `code`, or attribution
6
+ * params. Every later bare 302 in the chain inherits the query the same way,
7
+ * so no redirect-only route can shed it. Netlify leaves a 200 alone: a
8
+ * navigation gets an HTML page that replaces itself with the clean Location.
9
+ */
10
+ export declare function queryEchoSafeRedirect(event: H3Event, response: Response): Response;
@@ -0,0 +1,102 @@
1
+ import { getHeader } from "h3";
2
+ import { getAppBasePathFromViteEnv } from "./app-base-path.js";
3
+ function requestHasQuery(event) {
4
+ if (event.url?.search)
5
+ return true;
6
+ const raw = event.node?.req?.url ??
7
+ (typeof event.path === "string" ? event.path : "");
8
+ const queryStart = raw.indexOf("?");
9
+ return queryStart >= 0 && queryStart < raw.length - 1;
10
+ }
11
+ function isDocumentNavigation(event) {
12
+ const mode = getHeader(event, "sec-fetch-mode");
13
+ if (mode)
14
+ return mode === "navigate";
15
+ return (getHeader(event, "accept") ?? "").includes("text/html");
16
+ }
17
+ const PATH_BASE = "http://an.invalid";
18
+ // The page navigates with `location.replace`, which runs a `javascript:` URL
19
+ // that a 302 would refuse, so only http(s) destinations qualify. A path is
20
+ // resolved rather than pattern-matched: the URL parser drops tabs and
21
+ // newlines, so `/\t/evil.test` names another host.
22
+ function bareHttpDestination(location) {
23
+ try {
24
+ if (location.startsWith("/")) {
25
+ const target = new URL(location, PATH_BASE);
26
+ if (target.origin !== PATH_BASE || target.search)
27
+ return null;
28
+ return withAppBasePath(location);
29
+ }
30
+ const target = new URL(location);
31
+ if (target.protocol !== "https:" && target.protocol !== "http:") {
32
+ return null;
33
+ }
34
+ return target.search ? null : location;
35
+ // coercion-ok: a Location the URL parser rejects is not ours to rewrite.
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ // Workspace mounts and the dev gateway prefix an app's path Locations with its
42
+ // base path, but only on 3xx responses; the page has to carry the prefix.
43
+ function withAppBasePath(path) {
44
+ const base = getAppBasePathFromViteEnv();
45
+ if (!base)
46
+ return path;
47
+ const pathname = path.split(/[?#]/, 1)[0] || path;
48
+ if (pathname === base ||
49
+ pathname === `${base}.data` ||
50
+ pathname.startsWith(`${base}/`)) {
51
+ return path;
52
+ }
53
+ return pathname === "/" ? `${base}${path.slice(1)}` : `${base}${path}`;
54
+ }
55
+ function escapeHtmlAttr(value) {
56
+ return value
57
+ .replace(/&/g, "&amp;")
58
+ .replace(/</g, "&lt;")
59
+ .replace(/>/g, "&gt;")
60
+ .replace(/"/g, "&quot;");
61
+ }
62
+ /**
63
+ * Netlify's proxy copies the request's query string onto any redirect whose
64
+ * Location has none, so a bare 302 out of a sign-in callback lands the browser
65
+ * on a page still carrying the one-time `token`, OAuth `code`, or attribution
66
+ * params. Every later bare 302 in the chain inherits the query the same way,
67
+ * so no redirect-only route can shed it. Netlify leaves a 200 alone: a
68
+ * navigation gets an HTML page that replaces itself with the clean Location.
69
+ */
70
+ export function queryEchoSafeRedirect(event, response) {
71
+ // 307 and 308 replay the request method; a page that navigates would turn a
72
+ // POST into a GET.
73
+ if (![301, 302, 303].includes(response.status))
74
+ return response;
75
+ const location = response.headers.get("location");
76
+ if (!location || !requestHasQuery(event) || !isDocumentNavigation(event)) {
77
+ return response;
78
+ }
79
+ const destination = bareHttpDestination(location);
80
+ if (!destination)
81
+ return response;
82
+ const headers = new Headers();
83
+ for (const [key, value] of response.headers.entries()) {
84
+ const name = key.toLowerCase();
85
+ if (name === "location" ||
86
+ name === "set-cookie" ||
87
+ name === "content-length" ||
88
+ name === "content-type") {
89
+ continue;
90
+ }
91
+ headers.append(key, value);
92
+ }
93
+ for (const cookie of response.headers.getSetCookie()) {
94
+ headers.append("set-cookie", cookie);
95
+ }
96
+ headers.set("cache-control", "no-store");
97
+ headers.set("content-type", "text/html; charset=utf-8");
98
+ headers.set("referrer-policy", "no-referrer");
99
+ const href = escapeHtmlAttr(destination);
100
+ const script = JSON.stringify(destination).replace(/</g, "\\u003c");
101
+ return new Response(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta http-equiv="refresh" content="0;url=${href}"><script>location.replace(${script})</script></head><body></body></html>`, { status: 200, headers });
102
+ }
@@ -261,6 +261,10 @@ const FRAMEWORK_SCHEMA_ENSURES = [
261
261
  "SearchIndex",
262
262
  () => import("../search/index-store.js").then((m) => m.ensureSearchIndexTables()),
263
263
  ],
264
+ [
265
+ "ServicePrincipalPolicies",
266
+ () => import("../org/service-principal-policy.js").then((m) => m.ensureTable()),
267
+ ],
264
268
  [
265
269
  "Settings",
266
270
  () => import("../settings/store.js").then((m) => m.ensureTable()),
@@ -11,6 +11,7 @@ import { getSession, redirectWithStagedCookies, safeReturnPath, } from "./auth.j
11
11
  import { resolveSecret } from "./credential-provider.js";
12
12
  import { canonicalFrameworkPathname } from "./framework-route-prefix.js";
13
13
  import { decodeOAuthState, encodeOAuthState, getAppUrl, logOAuthStateDecodeFailure, oauthErrorPage, resolveOAuthRedirectUri, } from "./google-oauth.js";
14
+ import { queryEchoSafeRedirect } from "./query-echo-safe-redirect.js";
14
15
  import { runWithRequestContext } from "./request-context.js";
15
16
  const SUPPORTED_PROVIDERS = new Set([
16
17
  "figma",
@@ -342,7 +343,7 @@ export async function handleWorkspaceProviderOAuthCallback(event, providerId) {
342
343
  const returnPath = flow.returnUrl ??
343
344
  state.returnUrl ??
344
345
  `/settings/integrations?connected=${encodeURIComponent(providerId)}`;
345
- return redirectWithStagedCookies(event, getAppUrl(event, returnPath));
346
+ return queryEchoSafeRedirect(event, redirectWithStagedCookies(event, getAppUrl(event, returnPath)));
346
347
  });
347
348
  }
348
349
  export function buildWorkspaceProviderAuthorizationUrl(input) {
@@ -16,6 +16,7 @@ Rules:
16
16
  - To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
17
17
  - To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
18
18
  - If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
19
+ - MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
19
20
 
20
21
  Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
21
22
 
@@ -16,6 +16,7 @@ Rules:
16
16
  - To override the inference on an unusual action (e.g. a `POST` that only reads), pass `readOnly: true` on the action definition.
17
17
  - To let a mutating action run concurrently with other same-turn tool calls, pass `parallelSafe: true`. Only do this when the action is internally concurrency-safe and order-independent (for example, it uses an app-level lock or idempotent upsert semantics). Mutating actions remain serialized by default.
18
18
  - If a successful call hands control to the user — it renders a question, an intake form, or anything else the turn must wait on — pass `endsTurn: true` and make it the first or only call in the turn. It stops subsequent model steps and later calls, but cannot undo calls emitted before it. Without it a completion guard sees a turn that legitimately paused as one that failed to finish. `endsTurn: true` also hides the action from MCP, WebMCP, and A2A callers by default (`isActionExposedToExternalAgents` in core `action.ts`) — an external host has no in-app chat to receive the answer on. Pass `mcpTool: true` alongside it only when an external caller can complete the same operation without that answer.
19
+ - MCP hosts read tool annotations to decide how carefully to treat a call. Every MCP catalog advertises declared `mcpAnnotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`, optional `idempotentHint`); otherwise `/mcp` derives only `readOnlyHint` and an approval-based `destructiveHint`. Declare them on any external write that removes, overwrites, or replaces user content — recoverable Trash included — and mark idempotency-key-guarded writes `idempotentHint: true`.
19
20
 
20
21
  Agents do NOT need to call `refresh-screen` after a normal action — it's already handled. `refresh-screen` is only needed when the agent mutates data via a path the framework can't see (e.g. writing to an external system the app mirrors) or when the agent wants to pass a `scope` hint for narrower invalidation.
21
22