@agent-native/core 0.98.4 → 0.98.6

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 (314) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +37 -0
  4. package/corpus/core/docs/content/external-agents.mdx +62 -7
  5. package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +38 -7
  6. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +2 -0
  7. package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +39 -7
  8. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +2 -0
  9. package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +39 -7
  10. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +2 -0
  11. package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +39 -7
  12. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +4 -2
  13. package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +38 -7
  14. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +2 -0
  15. package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +38 -7
  16. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +2 -0
  17. package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +38 -7
  18. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +2 -0
  19. package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +39 -7
  20. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +2 -0
  21. package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +37 -7
  22. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +2 -0
  23. package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  24. package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +37 -7
  25. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +2 -0
  26. package/corpus/core/docs/content/tracking.mdx +2 -0
  27. package/corpus/core/package.json +1 -1
  28. package/corpus/core/src/a2a/handlers.ts +95 -21
  29. package/corpus/core/src/a2a/task-store.ts +65 -3
  30. package/corpus/core/src/agent/durable-background.ts +16 -16
  31. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +25 -8
  32. package/corpus/core/src/agent/engine/anthropic-engine.ts +17 -3
  33. package/corpus/core/src/agent/engine/output-tokens.ts +6 -6
  34. package/corpus/core/src/agent/production-agent.ts +200 -27
  35. package/corpus/core/src/agent/run-manager.ts +81 -1
  36. package/corpus/core/src/agent/run-store.ts +301 -41
  37. package/corpus/core/src/client/AgentPanel.tsx +6 -2
  38. package/corpus/core/src/client/AssistantChat.tsx +364 -69
  39. package/corpus/core/src/client/agent-chat-adapter.ts +169 -1
  40. package/corpus/core/src/client/chat/message-components.tsx +3 -0
  41. package/corpus/core/src/client/chat/tool-call-display.tsx +20 -36
  42. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +7 -0
  43. package/corpus/core/src/client/extensions/AgentNativeExtensionFrame.tsx +2 -0
  44. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +2 -0
  45. package/corpus/core/src/client/extensions/ExtensionEditor.tsx +9 -2
  46. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +2 -0
  47. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +2 -0
  48. package/corpus/core/src/client/extensions/portable-extension.ts +2 -0
  49. package/corpus/core/src/client/session-replay.ts +361 -29
  50. package/corpus/core/src/client/sse-event-processor.ts +43 -1
  51. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +1 -1
  52. package/corpus/core/src/demo/config.ts +6 -6
  53. package/corpus/core/src/demo/fetch-interceptor.ts +13 -4
  54. package/corpus/core/src/demo/redact.ts +13 -149
  55. package/corpus/core/src/extensions/html-shell.ts +3 -0
  56. package/corpus/core/src/extensions/session-replay-iframe.ts +131 -0
  57. package/corpus/core/src/integrations/adapters/slack.ts +123 -13
  58. package/corpus/core/src/integrations/identity-links-store.ts +210 -0
  59. package/corpus/core/src/integrations/identity.ts +132 -0
  60. package/corpus/core/src/integrations/index.ts +6 -0
  61. package/corpus/core/src/integrations/plugin.ts +58 -1
  62. package/corpus/core/src/integrations/types.ts +7 -0
  63. package/corpus/core/src/mcp/build-server.ts +127 -22
  64. package/corpus/core/src/mcp/external-agent-policy.ts +18 -0
  65. package/corpus/core/src/mcp/index.ts +1 -0
  66. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -20
  67. package/corpus/core/src/server/agent-chat/plugin-options.ts +12 -1
  68. package/corpus/core/src/server/agent-chat/script-entries.ts +20 -3
  69. package/corpus/core/src/server/agent-chat-plugin.ts +3 -0
  70. package/corpus/core/src/session-replay-iframe-protocol.ts +58 -0
  71. package/corpus/core/src/shared/reasoning-effort.ts +35 -0
  72. package/corpus/core/src/styles/agent-conversation.css +26 -0
  73. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  74. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  75. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +30 -21
  76. package/corpus/templates/analytics/AGENTS.md +27 -0
  77. package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +0 -1
  78. package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
  79. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
  80. package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +40 -0
  81. package/corpus/templates/analytics/actions/list-error-issues.ts +13 -0
  82. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
  83. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -0
  84. package/corpus/templates/analytics/app/global.css +2 -2
  85. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -4
  86. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +52 -203
  87. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +69 -16
  88. package/corpus/templates/analytics/changelog/2026-07-12-analytics-chat-retries-before-showing-a-generic-no-data-mess.md +6 -0
  89. package/corpus/templates/analytics/changelog/2026-07-12-analytics-sessions-search-no-longer-loses-fast-keystrokes-an.md +6 -0
  90. package/corpus/templates/analytics/changelog/2026-07-12-chart-selection-respects-chat-state.md +6 -0
  91. package/corpus/templates/analytics/changelog/2026-07-12-connected-external-agents-can-now-look-up-sessions-error-iss.md +6 -0
  92. package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-anonymizes-error-reporting-emails.md +6 -0
  93. package/corpus/templates/analytics/changelog/2026-07-12-session-identities-stay-visible-when-demo-mode-is-off.md +6 -0
  94. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-show-iframe-content.md +6 -0
  95. package/corpus/templates/analytics/netlify.toml +0 -3
  96. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +14 -0
  97. package/corpus/templates/analytics/server/lib/error-capture.ts +101 -2
  98. package/corpus/templates/analytics/server/lib/real-data-actions.ts +24 -0
  99. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +49 -17
  100. package/corpus/templates/analytics/server/lib/session-replay.ts +18 -16
  101. package/corpus/templates/analytics/server/plugins/agent-chat.ts +43 -6
  102. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  103. package/corpus/templates/assets/app/global.css +1 -1
  104. package/corpus/templates/brain/app/global.css +2 -2
  105. package/corpus/templates/brain/changelog/2026-07-12-fix-long-response-progress.md +6 -0
  106. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +1 -1
  107. package/corpus/templates/forms/app/components/ThemeToggle.tsx +22 -6
  108. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +5 -5
  109. package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +3 -3
  110. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +16 -16
  111. package/corpus/templates/forms/app/global.css +164 -13
  112. package/corpus/templates/forms/app/pages/AskPage.tsx +35 -13
  113. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +143 -67
  114. package/corpus/templates/forms/app/pages/FormFillPage.tsx +44 -11
  115. package/corpus/templates/forms/app/pages/FormsListPage.tsx +42 -24
  116. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +28 -15
  117. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +46 -20
  118. package/corpus/templates/forms/app/routes/_app.settings.tsx +1 -1
  119. package/corpus/templates/forms/changelog/2026-07-12-ask-forms-suggestions-now-sit-beneath-the-composer-for-a-tig.md +6 -0
  120. package/corpus/templates/forms/changelog/2026-07-12-builder-response-tables-now-fill-the-view-without-a-redundan.md +6 -0
  121. package/corpus/templates/forms/changelog/2026-07-12-form-builder-surfaces-are-calmer-fields-stay-stable-on-hover.md +6 -0
  122. package/corpus/templates/forms/changelog/2026-07-12-response-tables-now-fill-the-available-pane-with-a-flexible-.md +6 -0
  123. package/corpus/templates/forms/changelog/2026-07-12-smoother-more-tactile-controls-throughout-animated-icon-and-.md +6 -0
  124. package/corpus/templates/mail/AGENTS.md +3 -1
  125. package/corpus/templates/mail/app/components/email/EmailThread.tsx +21 -13
  126. package/corpus/templates/mail/app/components/email/email-iframe-document.ts +18 -0
  127. package/corpus/templates/mail/server/lib/mail-integrations.ts +3 -0
  128. package/dist/a2a/handlers.d.ts.map +1 -1
  129. package/dist/a2a/handlers.js +84 -15
  130. package/dist/a2a/handlers.js.map +1 -1
  131. package/dist/a2a/task-store.d.ts +20 -1
  132. package/dist/a2a/task-store.d.ts.map +1 -1
  133. package/dist/a2a/task-store.js +57 -4
  134. package/dist/a2a/task-store.js.map +1 -1
  135. package/dist/agent/durable-background.d.ts +10 -10
  136. package/dist/agent/durable-background.d.ts.map +1 -1
  137. package/dist/agent/durable-background.js +16 -16
  138. package/dist/agent/durable-background.js.map +1 -1
  139. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  140. package/dist/agent/engine/ai-sdk-engine.js +19 -8
  141. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  142. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  143. package/dist/agent/engine/anthropic-engine.js +11 -3
  144. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  145. package/dist/agent/engine/output-tokens.js +6 -6
  146. package/dist/agent/engine/output-tokens.js.map +1 -1
  147. package/dist/agent/production-agent.d.ts +52 -0
  148. package/dist/agent/production-agent.d.ts.map +1 -1
  149. package/dist/agent/production-agent.js +154 -26
  150. package/dist/agent/production-agent.js.map +1 -1
  151. package/dist/agent/run-manager.d.ts +9 -0
  152. package/dist/agent/run-manager.d.ts.map +1 -1
  153. package/dist/agent/run-manager.js +71 -1
  154. package/dist/agent/run-manager.js.map +1 -1
  155. package/dist/agent/run-store.d.ts +9 -0
  156. package/dist/agent/run-store.d.ts.map +1 -1
  157. package/dist/agent/run-store.js +243 -48
  158. package/dist/agent/run-store.js.map +1 -1
  159. package/dist/client/AgentPanel.d.ts.map +1 -1
  160. package/dist/client/AgentPanel.js +6 -2
  161. package/dist/client/AgentPanel.js.map +1 -1
  162. package/dist/client/AssistantChat.d.ts +21 -0
  163. package/dist/client/AssistantChat.d.ts.map +1 -1
  164. package/dist/client/AssistantChat.js +298 -67
  165. package/dist/client/AssistantChat.js.map +1 -1
  166. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  167. package/dist/client/agent-chat-adapter.js +146 -1
  168. package/dist/client/agent-chat-adapter.js.map +1 -1
  169. package/dist/client/chat/message-components.d.ts.map +1 -1
  170. package/dist/client/chat/message-components.js +2 -1
  171. package/dist/client/chat/message-components.js.map +1 -1
  172. package/dist/client/chat/tool-call-display.d.ts +3 -1
  173. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  174. package/dist/client/chat/tool-call-display.js +13 -37
  175. package/dist/client/chat/tool-call-display.js.map +1 -1
  176. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +1 -0
  177. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  178. package/dist/client/conversation/use-near-bottom-autoscroll.js +6 -0
  179. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  180. package/dist/client/extensions/AgentNativeExtensionFrame.d.ts.map +1 -1
  181. package/dist/client/extensions/AgentNativeExtensionFrame.js +2 -1
  182. package/dist/client/extensions/AgentNativeExtensionFrame.js.map +1 -1
  183. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  184. package/dist/client/extensions/EmbeddedExtension.js +2 -1
  185. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  186. package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
  187. package/dist/client/extensions/ExtensionEditor.js +5 -2
  188. package/dist/client/extensions/ExtensionEditor.js.map +1 -1
  189. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  190. package/dist/client/extensions/ExtensionViewer.js +2 -1
  191. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  192. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -1
  193. package/dist/client/extensions/InlineExtensionFrame.js +2 -1
  194. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -1
  195. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  196. package/dist/client/extensions/portable-extension.js +2 -0
  197. package/dist/client/extensions/portable-extension.js.map +1 -1
  198. package/dist/client/session-replay.d.ts.map +1 -1
  199. package/dist/client/session-replay.js +308 -29
  200. package/dist/client/session-replay.js.map +1 -1
  201. package/dist/client/sse-event-processor.d.ts +5 -0
  202. package/dist/client/sse-event-processor.d.ts.map +1 -1
  203. package/dist/client/sse-event-processor.js +37 -1
  204. package/dist/client/sse-event-processor.js.map +1 -1
  205. package/dist/collab/routes.d.ts +1 -1
  206. package/dist/demo/actions/toggle-demo-mode.js +1 -1
  207. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  208. package/dist/demo/config.js +6 -6
  209. package/dist/demo/config.js.map +1 -1
  210. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  211. package/dist/demo/fetch-interceptor.js +13 -4
  212. package/dist/demo/fetch-interceptor.js.map +1 -1
  213. package/dist/demo/redact.d.ts +7 -7
  214. package/dist/demo/redact.d.ts.map +1 -1
  215. package/dist/demo/redact.js +8 -145
  216. package/dist/demo/redact.js.map +1 -1
  217. package/dist/extensions/html-shell.d.ts.map +1 -1
  218. package/dist/extensions/html-shell.js +2 -0
  219. package/dist/extensions/html-shell.js.map +1 -1
  220. package/dist/extensions/session-replay-iframe.d.ts +10 -0
  221. package/dist/extensions/session-replay-iframe.d.ts.map +1 -0
  222. package/dist/extensions/session-replay-iframe.js +121 -0
  223. package/dist/extensions/session-replay-iframe.js.map +1 -0
  224. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  225. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  226. package/dist/integrations/adapters/slack.js +98 -13
  227. package/dist/integrations/adapters/slack.js.map +1 -1
  228. package/dist/integrations/identity-links-store.d.ts +23 -0
  229. package/dist/integrations/identity-links-store.d.ts.map +1 -0
  230. package/dist/integrations/identity-links-store.js +158 -0
  231. package/dist/integrations/identity-links-store.js.map +1 -0
  232. package/dist/integrations/identity.d.ts +11 -0
  233. package/dist/integrations/identity.d.ts.map +1 -0
  234. package/dist/integrations/identity.js +100 -0
  235. package/dist/integrations/identity.js.map +1 -0
  236. package/dist/integrations/index.d.ts +2 -0
  237. package/dist/integrations/index.d.ts.map +1 -1
  238. package/dist/integrations/index.js +2 -0
  239. package/dist/integrations/index.js.map +1 -1
  240. package/dist/integrations/plugin.d.ts.map +1 -1
  241. package/dist/integrations/plugin.js +43 -1
  242. package/dist/integrations/plugin.js.map +1 -1
  243. package/dist/integrations/types.d.ts +6 -0
  244. package/dist/integrations/types.d.ts.map +1 -1
  245. package/dist/integrations/types.js.map +1 -1
  246. package/dist/mcp/build-server.d.ts +9 -2
  247. package/dist/mcp/build-server.d.ts.map +1 -1
  248. package/dist/mcp/build-server.js +97 -20
  249. package/dist/mcp/build-server.js.map +1 -1
  250. package/dist/mcp/external-agent-policy.d.ts +19 -0
  251. package/dist/mcp/external-agent-policy.d.ts.map +1 -0
  252. package/dist/mcp/external-agent-policy.js +2 -0
  253. package/dist/mcp/external-agent-policy.js.map +1 -0
  254. package/dist/mcp/index.d.ts +1 -0
  255. package/dist/mcp/index.d.ts.map +1 -1
  256. package/dist/mcp/index.js.map +1 -1
  257. package/dist/notifications/routes.d.ts +1 -1
  258. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  259. package/dist/scripts/agent-engines/list-agent-engines.js +1 -19
  260. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  261. package/dist/server/agent-chat/plugin-options.d.ts +11 -1
  262. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  263. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  264. package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
  265. package/dist/server/agent-chat/script-entries.js +16 -2
  266. package/dist/server/agent-chat/script-entries.js.map +1 -1
  267. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  268. package/dist/server/agent-chat-plugin.js +3 -0
  269. package/dist/server/agent-chat-plugin.js.map +1 -1
  270. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  271. package/dist/server/transcribe-voice.d.ts +1 -1
  272. package/dist/session-replay-iframe-protocol.d.ts +36 -0
  273. package/dist/session-replay-iframe-protocol.d.ts.map +1 -0
  274. package/dist/session-replay-iframe-protocol.js +24 -0
  275. package/dist/session-replay-iframe-protocol.js.map +1 -0
  276. package/dist/shared/reasoning-effort.d.ts +11 -0
  277. package/dist/shared/reasoning-effort.d.ts.map +1 -1
  278. package/dist/shared/reasoning-effort.js +36 -0
  279. package/dist/shared/reasoning-effort.js.map +1 -1
  280. package/dist/styles/agent-conversation.css +26 -0
  281. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  282. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  283. package/docs/content/durable-background-runs.mdx +37 -0
  284. package/docs/content/external-agents.mdx +62 -7
  285. package/docs/content/locales/ar-SA/external-agents.mdx +38 -7
  286. package/docs/content/locales/ar-SA/tracking.mdx +2 -0
  287. package/docs/content/locales/de-DE/external-agents.mdx +39 -7
  288. package/docs/content/locales/de-DE/tracking.mdx +2 -0
  289. package/docs/content/locales/es-ES/external-agents.mdx +39 -7
  290. package/docs/content/locales/es-ES/tracking.mdx +2 -0
  291. package/docs/content/locales/fr-FR/external-agents.mdx +39 -7
  292. package/docs/content/locales/fr-FR/tracking.mdx +4 -2
  293. package/docs/content/locales/hi-IN/external-agents.mdx +38 -7
  294. package/docs/content/locales/hi-IN/tracking.mdx +2 -0
  295. package/docs/content/locales/ja-JP/external-agents.mdx +38 -7
  296. package/docs/content/locales/ja-JP/tracking.mdx +2 -0
  297. package/docs/content/locales/ko-KR/external-agents.mdx +38 -7
  298. package/docs/content/locales/ko-KR/tracking.mdx +2 -0
  299. package/docs/content/locales/pt-BR/external-agents.mdx +39 -7
  300. package/docs/content/locales/pt-BR/tracking.mdx +2 -0
  301. package/docs/content/locales/zh-CN/external-agents.mdx +37 -7
  302. package/docs/content/locales/zh-CN/tracking.mdx +2 -0
  303. package/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  304. package/docs/content/locales/zh-TW/external-agents.mdx +37 -7
  305. package/docs/content/locales/zh-TW/tracking.mdx +2 -0
  306. package/docs/content/tracking.mdx +2 -0
  307. package/package.json +2 -2
  308. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  309. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  310. package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
  311. package/dist/observability/hosted-model-experiment.d.ts +0 -39
  312. package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
  313. package/dist/observability/hosted-model-experiment.js +0 -90
  314. package/dist/observability/hosted-model-experiment.js.map +0 -1
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Durable links between a verified provider identity and an Agent Native user.
3
+ * Provider credentials and raw provider payloads never belong in this table.
4
+ */
5
+
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ import {
9
+ getDbExec,
10
+ intType,
11
+ isPostgres,
12
+ isUniqueViolation,
13
+ retryOnDdlRace,
14
+ } from "../db/client.js";
15
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
16
+
17
+ const TABLE = "integration_identity_links";
18
+ let _initPromise: Promise<void> | undefined;
19
+
20
+ export interface IntegrationIdentityLink {
21
+ id: string;
22
+ platform: string;
23
+ tenantId: string;
24
+ externalUserId: string;
25
+ userEmail: string;
26
+ orgId: string;
27
+ createdAt: number;
28
+ updatedAt: number;
29
+ }
30
+
31
+ function createSql(): string {
32
+ const integer = intType();
33
+ return `CREATE TABLE IF NOT EXISTS ${TABLE} (
34
+ id TEXT PRIMARY KEY,
35
+ platform TEXT NOT NULL,
36
+ tenant_id TEXT NOT NULL,
37
+ external_user_id TEXT NOT NULL,
38
+ user_email TEXT NOT NULL,
39
+ org_id TEXT NOT NULL,
40
+ created_at ${integer} NOT NULL,
41
+ updated_at ${integer} NOT NULL
42
+ )`;
43
+ }
44
+
45
+ const INDEXES = [
46
+ [
47
+ "idx_integration_identity_links_external",
48
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_integration_identity_links_external
49
+ ON ${TABLE}(platform, tenant_id, external_user_id)`,
50
+ ],
51
+ [
52
+ "idx_integration_identity_links_account",
53
+ `CREATE INDEX IF NOT EXISTS idx_integration_identity_links_account
54
+ ON ${TABLE}(org_id, user_email)`,
55
+ ],
56
+ ] as const;
57
+
58
+ async function ensureTable(): Promise<void> {
59
+ if (!_initPromise) {
60
+ _initPromise = (async () => {
61
+ const client = getDbExec();
62
+ if (isPostgres()) {
63
+ await ensureTableExists(TABLE, createSql());
64
+ for (const [name, sql] of INDEXES) {
65
+ await ensureIndexExists(name, sql);
66
+ }
67
+ return;
68
+ }
69
+ await retryOnDdlRace(() => client.execute(createSql()));
70
+ for (const [, sql] of INDEXES) {
71
+ await retryOnDdlRace(() => client.execute(sql));
72
+ }
73
+ })().catch((error) => {
74
+ _initPromise = undefined;
75
+ throw error;
76
+ });
77
+ }
78
+ return _initPromise;
79
+ }
80
+
81
+ function required(value: string, name: string): string {
82
+ const normalized = value.trim();
83
+ if (!normalized) throw new Error(`${name} is required`);
84
+ if (normalized.length > 512) throw new Error(`${name} is too long`);
85
+ return normalized;
86
+ }
87
+
88
+ function normalizePlatform(value: string): string {
89
+ return required(value, "platform").toLowerCase();
90
+ }
91
+
92
+ function normalizeEmail(value: string): string {
93
+ const normalized = required(value, "userEmail").toLowerCase();
94
+ if (!normalized.includes("@")) throw new Error("userEmail must be an email");
95
+ return normalized;
96
+ }
97
+
98
+ function rowToLink(row: Record<string, unknown>): IntegrationIdentityLink {
99
+ return {
100
+ id: String(row.id),
101
+ platform: String(row.platform),
102
+ tenantId: String(row.tenant_id),
103
+ externalUserId: String(row.external_user_id),
104
+ userEmail: String(row.user_email),
105
+ orgId: String(row.org_id),
106
+ createdAt: Number(row.created_at ?? 0),
107
+ updatedAt: Number(row.updated_at ?? 0),
108
+ };
109
+ }
110
+
111
+ async function selectByExternalIdentity(
112
+ platform: string,
113
+ tenantId: string,
114
+ externalUserId: string,
115
+ ): Promise<IntegrationIdentityLink | null> {
116
+ await ensureTable();
117
+ const { rows } = await getDbExec().execute({
118
+ sql: `SELECT * FROM ${TABLE}
119
+ WHERE platform = ? AND tenant_id = ? AND external_user_id = ?
120
+ LIMIT 1`,
121
+ args: [platform, tenantId, externalUserId],
122
+ });
123
+ return rows[0] ? rowToLink(rows[0] as Record<string, unknown>) : null;
124
+ }
125
+
126
+ function matchesVerifiedLink(
127
+ row: IntegrationIdentityLink,
128
+ input: { userEmail: string; orgId: string },
129
+ ): boolean {
130
+ return row.userEmail === input.userEmail && row.orgId === input.orgId;
131
+ }
132
+
133
+ /** Persist a verified mapping and fail closed if it changes identity later. */
134
+ export async function upsertVerifiedIntegrationIdentity(input: {
135
+ platform: string;
136
+ tenantId: string;
137
+ externalUserId: string;
138
+ userEmail: string;
139
+ orgId: string;
140
+ }): Promise<IntegrationIdentityLink> {
141
+ const platform = normalizePlatform(input.platform);
142
+ const tenantId = required(input.tenantId, "tenantId");
143
+ const externalUserId = required(input.externalUserId, "externalUserId");
144
+ const userEmail = normalizeEmail(input.userEmail);
145
+ const orgId = required(input.orgId, "orgId");
146
+ const existing = await selectByExternalIdentity(
147
+ platform,
148
+ tenantId,
149
+ externalUserId,
150
+ );
151
+
152
+ if (existing) {
153
+ if (!matchesVerifiedLink(existing, { userEmail, orgId })) {
154
+ throw new Error(
155
+ "This provider identity is already linked to a different Agent Native account.",
156
+ );
157
+ }
158
+ const updatedAt = Date.now();
159
+ await getDbExec().execute({
160
+ sql: `UPDATE ${TABLE} SET updated_at = ? WHERE id = ?`,
161
+ args: [updatedAt, existing.id],
162
+ });
163
+ return { ...existing, updatedAt };
164
+ }
165
+
166
+ const id = randomUUID();
167
+ const now = Date.now();
168
+ try {
169
+ await getDbExec().execute({
170
+ sql: `INSERT INTO ${TABLE} (
171
+ id, platform, tenant_id, external_user_id, user_email, org_id,
172
+ created_at, updated_at
173
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
174
+ args: [
175
+ id,
176
+ platform,
177
+ tenantId,
178
+ externalUserId,
179
+ userEmail,
180
+ orgId,
181
+ now,
182
+ now,
183
+ ],
184
+ });
185
+ } catch (error) {
186
+ if (!isUniqueViolation(error)) throw error;
187
+ const raced = await selectByExternalIdentity(
188
+ platform,
189
+ tenantId,
190
+ externalUserId,
191
+ );
192
+ if (!raced || !matchesVerifiedLink(raced, { userEmail, orgId })) {
193
+ throw new Error(
194
+ "This provider identity is already linked to a different Agent Native account.",
195
+ );
196
+ }
197
+ return raced;
198
+ }
199
+
200
+ return {
201
+ id,
202
+ platform,
203
+ tenantId,
204
+ externalUserId,
205
+ userEmail,
206
+ orgId,
207
+ createdAt: now,
208
+ updatedAt: now,
209
+ };
210
+ }
@@ -0,0 +1,132 @@
1
+ import { getDbExec } from "../db/client.js";
2
+ import { upsertVerifiedIntegrationIdentity } from "./identity-links-store.js";
3
+ import {
4
+ getActiveIntegrationInstallationByKey,
5
+ getActiveIntegrationInstallationForTenant,
6
+ } from "./installations-store.js";
7
+ import { slackInstallationKey } from "./slack-oauth.js";
8
+ import type { IncomingMessage, IntegrationExecutionContext } from "./types.js";
9
+
10
+ function serviceOwner(platform: string): string {
11
+ return `integration@${platform}`;
12
+ }
13
+
14
+ function normalizedEmail(value: unknown): string | null {
15
+ if (typeof value !== "string") return null;
16
+ const email = value.trim().toLowerCase();
17
+ if (!email || email.length > 320 || !email.includes("@")) return null;
18
+ return email;
19
+ }
20
+
21
+ async function resolveSlackInstallation(incoming: IncomingMessage) {
22
+ const teamId =
23
+ typeof incoming.platformContext.teamId === "string"
24
+ ? incoming.platformContext.teamId
25
+ : incoming.tenantId;
26
+ const enterpriseId =
27
+ typeof incoming.platformContext.enterpriseId === "string"
28
+ ? incoming.platformContext.enterpriseId
29
+ : undefined;
30
+ const apiAppId =
31
+ typeof incoming.platformContext.apiAppId === "string"
32
+ ? incoming.platformContext.apiAppId
33
+ : undefined;
34
+ if (apiAppId && (teamId || enterpriseId)) {
35
+ const byKey = await getActiveIntegrationInstallationByKey(
36
+ "slack",
37
+ slackInstallationKey({ teamId, enterpriseId, apiAppId }),
38
+ );
39
+ if (byKey) return byKey;
40
+ }
41
+ return teamId || enterpriseId
42
+ ? getActiveIntegrationInstallationForTenant(
43
+ "slack",
44
+ teamId ?? enterpriseId!,
45
+ )
46
+ : null;
47
+ }
48
+
49
+ async function isMemberOfOrg(email: string, orgId: string): Promise<boolean> {
50
+ try {
51
+ const { rows } = await getDbExec().execute({
52
+ sql: `SELECT 1 FROM org_members
53
+ WHERE org_id = ? AND LOWER(email) = ?
54
+ LIMIT 1`,
55
+ args: [orgId, email],
56
+ });
57
+ return rows.length > 0;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Resolve the default integration principal.
65
+ *
66
+ * Slack DMs become a user principal only after the adapter has verified the
67
+ * sender email and the email is already a member of the managed installation's
68
+ * Agent Native organization. Shared channels deliberately stay service-scoped
69
+ * so a channel message cannot borrow one participant's private permissions.
70
+ */
71
+ export async function resolveDefaultIntegrationExecutionContext(
72
+ incoming: IncomingMessage,
73
+ ): Promise<IntegrationExecutionContext> {
74
+ const installation =
75
+ incoming.platform === "slack"
76
+ ? await resolveSlackInstallation(incoming)
77
+ : null;
78
+
79
+ if (incoming.platform !== "slack" || incoming.conversationType !== "dm") {
80
+ return {
81
+ ownerEmail: serviceOwner(incoming.platform),
82
+ orgId: installation?.orgId ?? null,
83
+ principalType: "service",
84
+ ...(installation?.id ? { installationId: installation.id } : {}),
85
+ };
86
+ }
87
+
88
+ const email = normalizedEmail(incoming.senderEmail);
89
+ if (
90
+ !email ||
91
+ incoming.senderVerified !== true ||
92
+ !incoming.senderId ||
93
+ !incoming.tenantId
94
+ ) {
95
+ throw new Error(
96
+ "Slack DM sender identity could not be verified; refusing to run with a service principal.",
97
+ );
98
+ }
99
+ if (
100
+ incoming.actorTrust?.memberType === "guest" ||
101
+ incoming.actorTrust?.memberType === "external"
102
+ ) {
103
+ throw new Error(
104
+ "External or guest Slack members cannot use personal Agent Native permissions.",
105
+ );
106
+ }
107
+ if (!installation?.orgId) {
108
+ throw new Error(
109
+ "Slack workspace is not connected to an Agent Native organization.",
110
+ );
111
+ }
112
+
113
+ if (!(await isMemberOfOrg(email, installation.orgId))) {
114
+ throw new Error(
115
+ "Slack DM sender is not a member of the connected Agent Native organization.",
116
+ );
117
+ }
118
+
119
+ const link = await upsertVerifiedIntegrationIdentity({
120
+ platform: incoming.platform,
121
+ tenantId: incoming.tenantId,
122
+ externalUserId: incoming.senderId,
123
+ userEmail: email,
124
+ orgId: installation.orgId,
125
+ });
126
+ return {
127
+ ownerEmail: link.userEmail,
128
+ orgId: link.orgId,
129
+ principalType: "user",
130
+ installationId: installation.id,
131
+ };
132
+ }
@@ -18,6 +18,12 @@ export type {
18
18
  } from "./types.js";
19
19
  export { assertPlatformCapability } from "./types.js";
20
20
 
21
+ export { resolveDefaultIntegrationExecutionContext } from "./identity.js";
22
+ export {
23
+ upsertVerifiedIntegrationIdentity,
24
+ type IntegrationIdentityLink,
25
+ } from "./identity-links-store.js";
26
+
21
27
  export {
22
28
  BUILT_IN_INTEGRATION_CATALOG,
23
29
  INTEGRATION_CATEGORIES,
@@ -53,6 +53,7 @@ import {
53
53
  startGoogleDocsPoller,
54
54
  handlePushNotification,
55
55
  } from "./google-docs-poller.js";
56
+ import { resolveDefaultIntegrationExecutionContext } from "./identity.js";
56
57
  import {
57
58
  disconnectIntegrationInstallation,
58
59
  listIntegrationInstallations,
@@ -2343,13 +2344,53 @@ export function createIntegrationsPlugin(
2343
2344
  return { error: `Integration ${platform} is not enabled` };
2344
2345
  }
2345
2346
 
2346
- const incoming = await withCredentialContext(credentialContext, () =>
2347
+ let incoming = await withCredentialContext(credentialContext, () =>
2347
2348
  adapter.parseIncomingMessage(event),
2348
2349
  );
2349
2350
  if (!incoming) {
2350
2351
  setResponseStatus(event, 200);
2351
2352
  return "ok";
2352
2353
  }
2354
+ if (adapter.hydrateIncomingIdentity) {
2355
+ try {
2356
+ incoming = await withCredentialContext(credentialContext, () =>
2357
+ adapter.hydrateIncomingIdentity!(incoming!),
2358
+ );
2359
+ } catch (err) {
2360
+ // Identity hydration is best-effort for platforms that have an
2361
+ // app-specific resolver. Slack's default DM resolver below will
2362
+ // still fail closed when the identity is absent or unverified.
2363
+ console.warn(
2364
+ `[integrations] Could not hydrate ${platform} sender identity:`,
2365
+ err instanceof Error ? err.message : err,
2366
+ );
2367
+ }
2368
+ }
2369
+ let defaultExecutionContext: IntegrationExecutionContext | null =
2370
+ null;
2371
+ if (
2372
+ incoming.platform === "slack" &&
2373
+ incoming.conversationType === "dm"
2374
+ ) {
2375
+ try {
2376
+ defaultExecutionContext = await withCredentialContext(
2377
+ credentialContext,
2378
+ () => resolveDefaultIntegrationExecutionContext(incoming!),
2379
+ );
2380
+ } catch (err) {
2381
+ // An app-specific resolver may intentionally support a legacy
2382
+ // Slack setup. When no override exists, the branch below fails
2383
+ // closed instead of falling back to a service principal.
2384
+ if (!options?.resolveExecutionContext && !options?.resolveOwner) {
2385
+ console.error(
2386
+ `[integrations] default Slack DM identity denied message:`,
2387
+ err,
2388
+ );
2389
+ setResponseStatus(event, 200);
2390
+ return "ok";
2391
+ }
2392
+ }
2393
+ }
2353
2394
  let executionContext: IntegrationExecutionContext = {
2354
2395
  ownerEmail: `integration@${platform}`,
2355
2396
  orgId: null as string | null,
@@ -2370,6 +2411,8 @@ export function createIntegrationsPlugin(
2370
2411
  setResponseStatus(event, 200);
2371
2412
  return "ok";
2372
2413
  }
2414
+ } else if (defaultExecutionContext) {
2415
+ executionContext = defaultExecutionContext;
2373
2416
  } else if (options?.resolveOwner) {
2374
2417
  try {
2375
2418
  executionContext.ownerEmail = await withCredentialContext(
@@ -2382,6 +2425,20 @@ export function createIntegrationsPlugin(
2382
2425
  err,
2383
2426
  );
2384
2427
  }
2428
+ } else {
2429
+ try {
2430
+ executionContext = await withCredentialContext(
2431
+ credentialContext,
2432
+ () => resolveDefaultIntegrationExecutionContext(incoming!),
2433
+ );
2434
+ } catch (err) {
2435
+ console.error(
2436
+ `[integrations] default execution identity denied message:`,
2437
+ err,
2438
+ );
2439
+ setResponseStatus(event, 200);
2440
+ return "ok";
2441
+ }
2385
2442
  }
2386
2443
  if (executionContext.scopeId) {
2387
2444
  incoming.integrationScopeId = executionContext.scopeId;
@@ -274,6 +274,13 @@ export interface PlatformAdapter {
274
274
  */
275
275
  hydrateIncomingMessage?(incoming: IncomingMessage): Promise<IncomingMessage>;
276
276
 
277
+ /**
278
+ * Hydrate only verified sender identity before execution-context selection.
279
+ * This runs on the provider acknowledgement path and must avoid fetching
280
+ * conversation history or file bodies.
281
+ */
282
+ hydrateIncomingIdentity?(incoming: IncomingMessage): Promise<IncomingMessage>;
283
+
277
284
  /**
278
285
  * Provider-specific response returned only after a message is verified and
279
286
  * durably enqueued. Discord uses this to return a type-5 deferred response
@@ -51,6 +51,7 @@ import {
51
51
  MCP_CONNECT_SCOPE,
52
52
  } from "./connect-store.js";
53
53
  import { MCP_APP_REQUEST_ORIGIN_CSP_SOURCE } from "./embed-app.js";
54
+ import type { ExternalAgentPolicy } from "./external-agent-policy.js";
54
55
  import {
55
56
  MCP_OAUTH_SCOPES,
56
57
  hasMcpOAuthScope,
@@ -124,8 +125,9 @@ export interface MCPConfig {
124
125
  * from connectors. It is no longer gated behind an environment variable, and
125
126
  * the catalog is never inferred from the client name/user-agent.
126
127
  *
127
- * `tool-search` stays available in the compact catalog so any trimmed tool is
128
- * reachable on demand. Callers who need the full surface up front opt in
128
+ * `tool-search` stays available in the compact catalog for discovery. A
129
+ * searched action still needs the connector catalog or authenticated-read
130
+ * policy before `tools/call`; callers who need the full surface up front opt in
129
131
  * explicitly with `agent-native connect --full-catalog` (embeds a
130
132
  * `catalog_scope: "full"` claim in the connect-minted JWT) or the
131
133
  * deployment-wide `AGENT_NATIVE_MCP_FULL_CATALOG=1` env override.
@@ -134,6 +136,11 @@ export interface MCPConfig {
134
136
  * setting it on `MCPConfig` directly; the plugin copies it through.
135
137
  */
136
138
  connectorCatalog?: string[];
139
+ /**
140
+ * Optional policy for automatically exposing explicitly annotated,
141
+ * authenticated read actions to external MCP callers.
142
+ */
143
+ externalAgents?: ExternalAgentPolicy;
137
144
  }
138
145
 
139
146
  /**
@@ -281,7 +288,7 @@ function explicitlyRequestsFullMcpCatalog(
281
288
  // (100k+ tokens) into a context window just because a client called itself
282
289
  // "code"/"cursor"/"codex" was a recurring footgun. Everything else gets the
283
290
  // connector/compact catalog plus `tool-search`, which keeps every tool
284
- // reachable on demand.
291
+ // discoverable; only permitted actions are callable without full opt-in.
285
292
  if (process.env.AGENT_NATIVE_MCP_FULL_CATALOG === "1") return true;
286
293
  return requestMeta?.fullCatalog === true;
287
294
  }
@@ -309,17 +316,101 @@ function warnFullCatalogServed(toolCount: number): void {
309
316
  );
310
317
  }
311
318
 
319
+ function isAuthenticatedReadAction(entry: ActionEntry): boolean {
320
+ return (
321
+ entry.http !== false &&
322
+ entry.http?.method === "GET" &&
323
+ entry.readOnly === true &&
324
+ entry.publicAgent?.expose === true &&
325
+ entry.publicAgent.readOnly === true &&
326
+ entry.publicAgent.requiresAuth === true
327
+ );
328
+ }
329
+
312
330
  /**
313
- * Returns true when the given action name is in the template's connector
314
- * catalog, OR is a builtin cross-app tool that is always included for
315
- * external connector clients. Builtin tool names from
316
- * `COMPACT_MCP_APP_CATALOG_BUILTINS` are always allowed since they are the
317
- * stable external-agent verb set.
331
+ * Hard exclusion list for the `authenticatedReads: "auto"` derivation ONLY
332
+ * (see `autoAuthenticatedReadNames` below). Explicit `connectorCatalog`
333
+ * entries are a deliberate, reviewed choice made by the app and are NOT
334
+ * affected by this list an app can still list any of these names in
335
+ * `connectorCatalog` on purpose.
336
+ *
337
+ * These are the footgun families this file's other comments already call
338
+ * out ("removes footguns (db-exec, seed-*, extension tools, browser-session
339
+ * tools, etc.)" above, and "keeps db-exec / seed-* / extension /
340
+ * browser-session footguns off the external surface" near the connector
341
+ * tier below): generic core SQL access, template demo/seed data, the
342
+ * extension-management suite, live browser-session control, and Context
343
+ * X-Ray internals. `isAuthenticatedReadAction` only inspects action
344
+ * metadata (http/readOnly/publicAgent flags) — nothing stops a future
345
+ * change from mis-annotating one of these with that exact flag set again,
346
+ * the way `db-query`/`db-schema` were briefly (and accidentally) annotated
347
+ * before it was caught in review. These names can never be auto-derived
348
+ * from metadata alone; exposing one to external callers requires an
349
+ * explicit `connectorCatalog` entry.
318
350
  */
319
- function isActionInConnectorCatalog(name: string, config: MCPConfig): boolean {
320
- if (COMPACT_MCP_APP_CATALOG_BUILTINS.has(name)) return true;
321
- if (!Array.isArray(config.connectorCatalog)) return false;
322
- return config.connectorCatalog.includes(name);
351
+ const AUTO_READ_EXCLUDED_ACTION_NAMES = new Set([
352
+ "db-query",
353
+ "db-schema",
354
+ "db-exec",
355
+ "db-patch",
356
+ "context-manifest-get",
357
+ "context-pin",
358
+ "context-evict",
359
+ "context-restore",
360
+ "context-report",
361
+ ]);
362
+
363
+ /**
364
+ * Substring/prefix patterns for excluded name *families* that aren't a
365
+ * fixed, enumerable set: `seed-*` varies per app/template, and the
366
+ * extension-management and browser-session tool suites use varying verb
367
+ * prefixes around a shared noun (e.g. `list-extensions`, `create-extension`,
368
+ * `hide-extension`; `list-browser-sessions`, `run-browser-session-action`) —
369
+ * so a leading-prefix match alone would miss most of them.
370
+ */
371
+ const AUTO_READ_EXCLUDED_ACTION_PATTERNS: RegExp[] = [
372
+ /^seed-/,
373
+ /extension/,
374
+ /browser-session/,
375
+ ];
376
+
377
+ function isAutoReadExcludedActionName(name: string): boolean {
378
+ return (
379
+ AUTO_READ_EXCLUDED_ACTION_NAMES.has(name) ||
380
+ AUTO_READ_EXCLUDED_ACTION_PATTERNS.some((pattern) => pattern.test(name))
381
+ );
382
+ }
383
+
384
+ function autoAuthenticatedReadNames(
385
+ actions: Record<string, ActionEntry>,
386
+ config: MCPConfig,
387
+ ): Set<string> {
388
+ if (config.externalAgents?.authenticatedReads !== "auto") return new Set();
389
+ return new Set(
390
+ Object.entries(actions)
391
+ .filter(
392
+ ([name, entry]) =>
393
+ isAuthenticatedReadAction(entry) &&
394
+ !isAutoReadExcludedActionName(name),
395
+ )
396
+ .map(([name]) => name),
397
+ );
398
+ }
399
+
400
+ function externalAgentDenySet(config: MCPConfig): Set<string> {
401
+ return new Set(
402
+ (config.externalAgents?.denyActions ?? [])
403
+ .map((name) => name.trim())
404
+ .filter(Boolean),
405
+ );
406
+ }
407
+
408
+ function externalAgentWritesAreAskAppOnly(config: MCPConfig): boolean {
409
+ return (
410
+ config.externalAgents?.writes === "ask_app_only" ||
411
+ (config.externalAgents?.authenticatedReads === "auto" &&
412
+ config.externalAgents?.writes !== "allowlisted")
413
+ );
323
414
  }
324
415
 
325
416
  interface ResolvedMcpAppResource {
@@ -1300,16 +1391,21 @@ export async function createMCPServerForRequest(
1300
1391
  ),
1301
1392
  )
1302
1393
  : visibleActions;
1394
+ const autoReadNames = autoAuthenticatedReadNames(visibleActions, config);
1395
+ const connectorNames = new Set([
1396
+ ...(config.connectorCatalog ?? []),
1397
+ ...autoReadNames,
1398
+ ]);
1399
+ const denyNames = externalAgentDenySet(config);
1400
+ const automaticConnectorPolicyActive =
1401
+ config.externalAgents?.authenticatedReads === "auto";
1303
1402
  // Connector-catalog tier: when a template declares a connector allow-list,
1304
- // serve exactly that curated surface (+ cross-app builtins + tool-search) to
1305
- // external callers unless they explicitly opted into the full catalog. This
1306
- // is active by default whenever a catalog is declared no env flag required —
1307
- // so the ~105-tool full catalog can never leak just because a deployment
1308
- // forgot to set one. It also keeps db-exec / seed-* / extension /
1403
+ // serve exactly that curated surface plus any explicitly annotated
1404
+ // authenticated reads from `externalAgents.authenticatedReads: "auto"`.
1405
+ // This stays compact by default and keeps db-exec / seed-* / extension /
1309
1406
  // browser-session footguns off the external surface.
1310
1407
  const connectorCatalogActive =
1311
- Array.isArray(config.connectorCatalog) &&
1312
- config.connectorCatalog.length > 0 &&
1408
+ (connectorNames.size > 0 || automaticConnectorPolicyActive) &&
1313
1409
  !fullCatalogRequested;
1314
1410
  // When the connector catalog is active, filter directly from visibleActions
1315
1411
  // rather than advertisedActionsBeforeConnector. This ensures the connector
@@ -1318,9 +1414,18 @@ export async function createMCPServerForRequest(
1318
1414
  // would have activated the compact catalog for the same caller.
1319
1415
  const advertisedActions = connectorCatalogActive
1320
1416
  ? Object.fromEntries(
1321
- Object.entries(visibleActions).filter(([name]) =>
1322
- isActionInConnectorCatalog(name, config),
1323
- ),
1417
+ Object.entries(visibleActions).filter(([name, entry]) => {
1418
+ if (denyNames.has(name)) return false;
1419
+ if (COMPACT_MCP_APP_CATALOG_BUILTINS.has(name)) return true;
1420
+ if (!connectorNames.has(name)) return false;
1421
+ if (
1422
+ externalAgentWritesAreAskAppOnly(config) &&
1423
+ entry.readOnly !== true
1424
+ ) {
1425
+ return false;
1426
+ }
1427
+ return true;
1428
+ }),
1324
1429
  )
1325
1430
  : advertisedActionsBeforeConnector;
1326
1431
  if (fullCatalogRequested) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Policy for the default authenticated external-agent connector surface.
3
+ *
4
+ * The policy is intentionally narrow: "auto" only discovers actions that
5
+ * explicitly opt into authenticated, read-only public-agent exposure. It
6
+ * never makes an unannotated action callable and never grants writes.
7
+ */
8
+ export interface ExternalAgentPolicy {
9
+ /** Automatically advertise authenticated GET/read-only public-agent actions. */
10
+ authenticatedReads?: "off" | "auto";
11
+ /**
12
+ * Keep writes behind `ask_app` by default. `allowlisted` preserves explicit
13
+ * connectorCatalog write exposure for apps that intentionally need it.
14
+ */
15
+ writes?: "ask_app_only" | "allowlisted";
16
+ /** Explicit action names to remove from the derived connector surface. */
17
+ denyActions?: string[];
18
+ }
@@ -10,6 +10,7 @@ export {
10
10
  buildLinkArtifacts,
11
11
  } from "./build-server.js";
12
12
  export type { MCPCallerIdentity, MCPRequestMeta } from "./build-server.js";
13
+ export type { ExternalAgentPolicy } from "./external-agent-policy.js";
13
14
 
14
15
  // stdio transport for `agent-native mcp serve` (Node-only).
15
16
  export { runMCPStdio } from "./stdio.js";