@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/durable-background-runs.mdx +37 -0
- package/corpus/core/docs/content/external-agents.mdx +62 -7
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +38 -7
- package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +2 -0
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/de-DE/tracking.mdx +2 -0
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/es-ES/tracking.mdx +2 -0
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +4 -2
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +38 -7
- package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +2 -0
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +38 -7
- package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +2 -0
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +38 -7
- package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +2 -0
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +37 -7
- package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +2 -0
- package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +37 -7
- package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +2 -0
- package/corpus/core/docs/content/tracking.mdx +2 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/handlers.ts +95 -21
- package/corpus/core/src/a2a/task-store.ts +65 -3
- package/corpus/core/src/agent/durable-background.ts +16 -16
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +25 -8
- package/corpus/core/src/agent/engine/anthropic-engine.ts +17 -3
- package/corpus/core/src/agent/engine/output-tokens.ts +6 -6
- package/corpus/core/src/agent/production-agent.ts +200 -27
- package/corpus/core/src/agent/run-manager.ts +81 -1
- package/corpus/core/src/agent/run-store.ts +301 -41
- package/corpus/core/src/client/AgentPanel.tsx +6 -2
- package/corpus/core/src/client/AssistantChat.tsx +364 -69
- package/corpus/core/src/client/agent-chat-adapter.ts +169 -1
- package/corpus/core/src/client/chat/message-components.tsx +3 -0
- package/corpus/core/src/client/chat/tool-call-display.tsx +20 -36
- package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +7 -0
- package/corpus/core/src/client/extensions/AgentNativeExtensionFrame.tsx +2 -0
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +2 -0
- package/corpus/core/src/client/extensions/ExtensionEditor.tsx +9 -2
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +2 -0
- package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +2 -0
- package/corpus/core/src/client/extensions/portable-extension.ts +2 -0
- package/corpus/core/src/client/session-replay.ts +361 -29
- package/corpus/core/src/client/sse-event-processor.ts +43 -1
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +1 -1
- package/corpus/core/src/demo/config.ts +6 -6
- package/corpus/core/src/demo/fetch-interceptor.ts +13 -4
- package/corpus/core/src/demo/redact.ts +13 -149
- package/corpus/core/src/extensions/html-shell.ts +3 -0
- package/corpus/core/src/extensions/session-replay-iframe.ts +131 -0
- package/corpus/core/src/integrations/adapters/slack.ts +123 -13
- package/corpus/core/src/integrations/identity-links-store.ts +210 -0
- package/corpus/core/src/integrations/identity.ts +132 -0
- package/corpus/core/src/integrations/index.ts +6 -0
- package/corpus/core/src/integrations/plugin.ts +58 -1
- package/corpus/core/src/integrations/types.ts +7 -0
- package/corpus/core/src/mcp/build-server.ts +127 -22
- package/corpus/core/src/mcp/external-agent-policy.ts +18 -0
- package/corpus/core/src/mcp/index.ts +1 -0
- package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -20
- package/corpus/core/src/server/agent-chat/plugin-options.ts +12 -1
- package/corpus/core/src/server/agent-chat/script-entries.ts +20 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +3 -0
- package/corpus/core/src/session-replay-iframe-protocol.ts +58 -0
- package/corpus/core/src/shared/reasoning-effort.ts +35 -0
- package/corpus/core/src/styles/agent-conversation.css +26 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +30 -21
- package/corpus/templates/analytics/AGENTS.md +27 -0
- package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +0 -1
- package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +40 -0
- package/corpus/templates/analytics/actions/list-error-issues.ts +13 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -0
- package/corpus/templates/analytics/app/global.css +2 -2
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -4
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +52 -203
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +69 -16
- package/corpus/templates/analytics/changelog/2026-07-12-analytics-chat-retries-before-showing-a-generic-no-data-mess.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-analytics-sessions-search-no-longer-loses-fast-keystrokes-an.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-chart-selection-respects-chat-state.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-connected-external-agents-can-now-look-up-sessions-error-iss.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-anonymizes-error-reporting-emails.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-session-identities-stay-visible-when-demo-mode-is-off.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-session-replays-show-iframe-content.md +6 -0
- package/corpus/templates/analytics/netlify.toml +0 -3
- package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +14 -0
- package/corpus/templates/analytics/server/lib/error-capture.ts +101 -2
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +24 -0
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +49 -17
- package/corpus/templates/analytics/server/lib/session-replay.ts +18 -16
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +43 -6
- package/corpus/templates/analytics/server/plugins/db.ts +15 -0
- package/corpus/templates/assets/app/global.css +1 -1
- package/corpus/templates/brain/app/global.css +2 -2
- package/corpus/templates/brain/changelog/2026-07-12-fix-long-response-progress.md +6 -0
- package/corpus/templates/forms/app/components/CloudUpgrade.tsx +1 -1
- package/corpus/templates/forms/app/components/ThemeToggle.tsx +22 -6
- package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +5 -5
- package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +3 -3
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +16 -16
- package/corpus/templates/forms/app/global.css +164 -13
- package/corpus/templates/forms/app/pages/AskPage.tsx +35 -13
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +143 -67
- package/corpus/templates/forms/app/pages/FormFillPage.tsx +44 -11
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +42 -24
- package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +28 -15
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +46 -20
- package/corpus/templates/forms/app/routes/_app.settings.tsx +1 -1
- package/corpus/templates/forms/changelog/2026-07-12-ask-forms-suggestions-now-sit-beneath-the-composer-for-a-tig.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-12-builder-response-tables-now-fill-the-view-without-a-redundan.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-12-form-builder-surfaces-are-calmer-fields-stay-stable-on-hover.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-12-response-tables-now-fill-the-available-pane-with-a-flexible-.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-12-smoother-more-tactile-controls-throughout-animated-icon-and-.md +6 -0
- package/corpus/templates/mail/AGENTS.md +3 -1
- package/corpus/templates/mail/app/components/email/EmailThread.tsx +21 -13
- package/corpus/templates/mail/app/components/email/email-iframe-document.ts +18 -0
- package/corpus/templates/mail/server/lib/mail-integrations.ts +3 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +84 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/task-store.d.ts +20 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +57 -4
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/agent/durable-background.d.ts +10 -10
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +16 -16
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +19 -8
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
- package/dist/agent/engine/anthropic-engine.js +11 -3
- package/dist/agent/engine/anthropic-engine.js.map +1 -1
- package/dist/agent/engine/output-tokens.js +6 -6
- package/dist/agent/engine/output-tokens.js.map +1 -1
- package/dist/agent/production-agent.d.ts +52 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +154 -26
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +9 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +71 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +9 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +243 -48
- package/dist/agent/run-store.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +6 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +21 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +298 -67
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +146 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +2 -1
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +3 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +13 -37
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +1 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
- package/dist/client/conversation/use-near-bottom-autoscroll.js +6 -0
- package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
- package/dist/client/extensions/AgentNativeExtensionFrame.d.ts.map +1 -1
- package/dist/client/extensions/AgentNativeExtensionFrame.js +2 -1
- package/dist/client/extensions/AgentNativeExtensionFrame.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +2 -1
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionEditor.js +5 -2
- package/dist/client/extensions/ExtensionEditor.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +2 -1
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -1
- package/dist/client/extensions/InlineExtensionFrame.js +2 -1
- package/dist/client/extensions/InlineExtensionFrame.js.map +1 -1
- package/dist/client/extensions/portable-extension.d.ts.map +1 -1
- package/dist/client/extensions/portable-extension.js +2 -0
- package/dist/client/extensions/portable-extension.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +308 -29
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +5 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +37 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/demo/actions/toggle-demo-mode.js +1 -1
- package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
- package/dist/demo/config.js +6 -6
- package/dist/demo/config.js.map +1 -1
- package/dist/demo/fetch-interceptor.d.ts.map +1 -1
- package/dist/demo/fetch-interceptor.js +13 -4
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/demo/redact.d.ts +7 -7
- package/dist/demo/redact.d.ts.map +1 -1
- package/dist/demo/redact.js +8 -145
- package/dist/demo/redact.js.map +1 -1
- package/dist/extensions/html-shell.d.ts.map +1 -1
- package/dist/extensions/html-shell.js +2 -0
- package/dist/extensions/html-shell.js.map +1 -1
- package/dist/extensions/session-replay-iframe.d.ts +10 -0
- package/dist/extensions/session-replay-iframe.d.ts.map +1 -0
- package/dist/extensions/session-replay-iframe.js +121 -0
- package/dist/extensions/session-replay-iframe.js.map +1 -0
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +98 -13
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/identity-links-store.d.ts +23 -0
- package/dist/integrations/identity-links-store.d.ts.map +1 -0
- package/dist/integrations/identity-links-store.js +158 -0
- package/dist/integrations/identity-links-store.js.map +1 -0
- package/dist/integrations/identity.d.ts +11 -0
- package/dist/integrations/identity.d.ts.map +1 -0
- package/dist/integrations/identity.js +100 -0
- package/dist/integrations/identity.js.map +1 -0
- package/dist/integrations/index.d.ts +2 -0
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +2 -0
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +43 -1
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +6 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/mcp/build-server.d.ts +9 -2
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +97 -20
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/external-agent-policy.d.ts +19 -0
- package/dist/mcp/external-agent-policy.d.ts.map +1 -0
- package/dist/mcp/external-agent-policy.js +2 -0
- package/dist/mcp/external-agent-policy.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
- package/dist/scripts/agent-engines/list-agent-engines.js +1 -19
- package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
- package/dist/server/agent-chat/plugin-options.d.ts +11 -1
- package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
- package/dist/server/agent-chat/plugin-options.js.map +1 -1
- package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
- package/dist/server/agent-chat/script-entries.js +16 -2
- package/dist/server/agent-chat/script-entries.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +3 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/session-replay-iframe-protocol.d.ts +36 -0
- package/dist/session-replay-iframe-protocol.d.ts.map +1 -0
- package/dist/session-replay-iframe-protocol.js +24 -0
- package/dist/session-replay-iframe-protocol.js.map +1 -0
- package/dist/shared/reasoning-effort.d.ts +11 -0
- package/dist/shared/reasoning-effort.d.ts.map +1 -1
- package/dist/shared/reasoning-effort.js +36 -0
- package/dist/shared/reasoning-effort.js.map +1 -1
- package/dist/styles/agent-conversation.css +26 -0
- package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
- package/docs/content/durable-background-runs.mdx +37 -0
- package/docs/content/external-agents.mdx +62 -7
- package/docs/content/locales/ar-SA/external-agents.mdx +38 -7
- package/docs/content/locales/ar-SA/tracking.mdx +2 -0
- package/docs/content/locales/de-DE/external-agents.mdx +39 -7
- package/docs/content/locales/de-DE/tracking.mdx +2 -0
- package/docs/content/locales/es-ES/external-agents.mdx +39 -7
- package/docs/content/locales/es-ES/tracking.mdx +2 -0
- package/docs/content/locales/fr-FR/external-agents.mdx +39 -7
- package/docs/content/locales/fr-FR/tracking.mdx +4 -2
- package/docs/content/locales/hi-IN/external-agents.mdx +38 -7
- package/docs/content/locales/hi-IN/tracking.mdx +2 -0
- package/docs/content/locales/ja-JP/external-agents.mdx +38 -7
- package/docs/content/locales/ja-JP/tracking.mdx +2 -0
- package/docs/content/locales/ko-KR/external-agents.mdx +38 -7
- package/docs/content/locales/ko-KR/tracking.mdx +2 -0
- package/docs/content/locales/pt-BR/external-agents.mdx +39 -7
- package/docs/content/locales/pt-BR/tracking.mdx +2 -0
- package/docs/content/locales/zh-CN/external-agents.mdx +37 -7
- package/docs/content/locales/zh-CN/tracking.mdx +2 -0
- package/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
- package/docs/content/locales/zh-TW/external-agents.mdx +37 -7
- package/docs/content/locales/zh-TW/tracking.mdx +2 -0
- package/docs/content/tracking.mdx +2 -0
- package/package.json +2 -2
- package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
- package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
- package/dist/observability/hosted-model-experiment.d.ts +0 -39
- package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
- package/dist/observability/hosted-model-experiment.js +0 -90
- 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
|
-
|
|
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
|
|
128
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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";
|