@agent-native/core 0.98.6 → 0.98.7

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 (80) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/docs/content/external-agents.mdx +15 -11
  4. package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +3 -1
  5. package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +3 -1
  6. package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +3 -1
  7. package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +3 -1
  8. package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +3 -1
  9. package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +3 -1
  10. package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +3 -1
  11. package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +3 -1
  12. package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +3 -1
  13. package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +3 -1
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/handlers.ts +6 -5
  16. package/corpus/core/src/a2a/task-store.ts +63 -0
  17. package/corpus/core/src/client/session-replay.ts +67 -5
  18. package/corpus/core/src/integrations/adapters/slack.ts +99 -19
  19. package/corpus/core/src/integrations/identity.ts +103 -39
  20. package/corpus/core/src/integrations/plugin.ts +81 -9
  21. package/corpus/core/src/integrations/types.ts +30 -0
  22. package/corpus/core/src/integrations/webhook-handler.ts +1 -0
  23. package/corpus/core/src/mcp/builtin-tools.ts +5 -2
  24. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +4 -1
  25. package/corpus/templates/analytics/AGENTS.md +9 -9
  26. package/corpus/templates/analytics/changelog/2026-07-12-analytics-uses-the-full-in-app-agent-for-multi-step-incident.md +6 -0
  27. package/corpus/templates/analytics/server/handlers/session-replay.ts +15 -9
  28. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +23 -5
  29. package/corpus/templates/analytics/server/lib/session-replay.ts +26 -5
  30. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -1
  31. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +8 -5
  32. package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +8 -3
  33. package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +12 -2
  34. package/dist/a2a/handlers.d.ts.map +1 -1
  35. package/dist/a2a/handlers.js +6 -6
  36. package/dist/a2a/handlers.js.map +1 -1
  37. package/dist/a2a/task-store.d.ts +12 -0
  38. package/dist/a2a/task-store.d.ts.map +1 -1
  39. package/dist/a2a/task-store.js +54 -0
  40. package/dist/a2a/task-store.js.map +1 -1
  41. package/dist/client/session-replay.d.ts.map +1 -1
  42. package/dist/client/session-replay.js +53 -8
  43. package/dist/client/session-replay.js.map +1 -1
  44. package/dist/collab/routes.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  47. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  48. package/dist/integrations/adapters/slack.js +85 -11
  49. package/dist/integrations/adapters/slack.js.map +1 -1
  50. package/dist/integrations/identity.d.ts +19 -2
  51. package/dist/integrations/identity.d.ts.map +1 -1
  52. package/dist/integrations/identity.js +76 -34
  53. package/dist/integrations/identity.js.map +1 -1
  54. package/dist/integrations/plugin.d.ts.map +1 -1
  55. package/dist/integrations/plugin.js +56 -6
  56. package/dist/integrations/plugin.js.map +1 -1
  57. package/dist/integrations/types.d.ts +25 -0
  58. package/dist/integrations/types.d.ts.map +1 -1
  59. package/dist/integrations/types.js.map +1 -1
  60. package/dist/integrations/webhook-handler.js +1 -0
  61. package/dist/integrations/webhook-handler.js.map +1 -1
  62. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  63. package/dist/mcp/builtin-tools.js +5 -2
  64. package/dist/mcp/builtin-tools.js.map +1 -1
  65. package/dist/progress/routes.d.ts +1 -1
  66. package/dist/resources/handlers.d.ts +3 -3
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/transcribe-voice.d.ts +1 -1
  69. package/docs/content/external-agents.mdx +15 -11
  70. package/docs/content/locales/ar-SA/external-agents.mdx +3 -1
  71. package/docs/content/locales/de-DE/external-agents.mdx +3 -1
  72. package/docs/content/locales/es-ES/external-agents.mdx +3 -1
  73. package/docs/content/locales/fr-FR/external-agents.mdx +3 -1
  74. package/docs/content/locales/hi-IN/external-agents.mdx +3 -1
  75. package/docs/content/locales/ja-JP/external-agents.mdx +3 -1
  76. package/docs/content/locales/ko-KR/external-agents.mdx +3 -1
  77. package/docs/content/locales/pt-BR/external-agents.mdx +3 -1
  78. package/docs/content/locales/zh-CN/external-agents.mdx +3 -1
  79. package/docs/content/locales/zh-TW/external-agents.mdx +3 -1
  80. package/package.json +1 -1
@@ -2,6 +2,22 @@ import { getDbExec } from "../db/client.js";
2
2
  import { upsertVerifiedIntegrationIdentity } from "./identity-links-store.js";
3
3
  import { getActiveIntegrationInstallationByKey, getActiveIntegrationInstallationForTenant, } from "./installations-store.js";
4
4
  import { slackInstallationKey } from "./slack-oauth.js";
5
+ /**
6
+ * Thrown when the default Slack DM identity ladder declines to run a message.
7
+ * `reason` is a stable machine-readable discriminator (used e.g. to dedupe
8
+ * decline replies); `userFacingMessage` is safe to send back to the sender as
9
+ * a polite reply; `message` stays log-only.
10
+ */
11
+ export class IntegrationIdentityDeclinedError extends Error {
12
+ reason;
13
+ userFacingMessage;
14
+ constructor(reason, message, userFacingMessage) {
15
+ super(message);
16
+ this.name = "IntegrationIdentityDeclinedError";
17
+ this.reason = reason;
18
+ this.userFacingMessage = userFacingMessage;
19
+ }
20
+ }
5
21
  function serviceOwner(platform) {
6
22
  return `integration@${platform}`;
7
23
  }
@@ -33,26 +49,26 @@ async function resolveSlackInstallation(incoming) {
33
49
  : null;
34
50
  }
35
51
  async function isMemberOfOrg(email, orgId) {
36
- try {
37
- const { rows } = await getDbExec().execute({
38
- sql: `SELECT 1 FROM org_members
52
+ const { rows } = await getDbExec().execute({
53
+ sql: `SELECT 1 FROM org_members
39
54
  WHERE org_id = ? AND LOWER(email) = ?
40
55
  LIMIT 1`,
41
- args: [orgId, email],
42
- });
43
- return rows.length > 0;
44
- }
45
- catch {
46
- return false;
47
- }
56
+ args: [orgId, email],
57
+ });
58
+ return rows.length > 0;
48
59
  }
49
60
  /**
50
61
  * Resolve the default integration principal.
51
62
  *
52
63
  * Slack DMs become a user principal only after the adapter has verified the
53
64
  * sender email and the email is already a member of the managed installation's
54
- * Agent Native organization. Shared channels deliberately stay service-scoped
55
- * so a channel message cannot borrow one participant's private permissions.
65
+ * Agent Native organization. Hydrated full workspace members whose email is
66
+ * missing, unverified, or not an organization member run as an anonymous
67
+ * org-scoped service principal instead — the same visibility shared channels
68
+ * get. Hydration failures, guests/external members, and workspaces without a
69
+ * connected organization are declined with a user-facing message. Shared
70
+ * channels deliberately stay service-scoped so a channel message cannot borrow
71
+ * one participant's private permissions.
56
72
  */
57
73
  export async function resolveDefaultIntegrationExecutionContext(incoming) {
58
74
  const installation = incoming.platform === "slack"
@@ -66,35 +82,61 @@ export async function resolveDefaultIntegrationExecutionContext(incoming) {
66
82
  ...(installation?.id ? { installationId: installation.id } : {}),
67
83
  };
68
84
  }
69
- const email = normalizedEmail(incoming.senderEmail);
70
- if (!email ||
71
- incoming.senderVerified !== true ||
72
- !incoming.senderId ||
73
- !incoming.tenantId) {
74
- throw new Error("Slack DM sender identity could not be verified; refusing to run with a service principal.");
85
+ // Hydration check first: a transient users.info failure must land on the
86
+ // retry decline here, never on the anonymous org-scoped tier below.
87
+ if (incoming.actorTrust?.verified !== true) {
88
+ throw new IntegrationIdentityDeclinedError("unverified", "Slack DM sender identity could not be hydrated; declining instead of guessing a principal.", "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.");
75
89
  }
76
- if (incoming.actorTrust?.memberType === "guest" ||
77
- incoming.actorTrust?.memberType === "external") {
78
- throw new Error("External or guest Slack members cannot use personal Agent Native permissions.");
90
+ if (incoming.actorTrust.memberType === "guest" ||
91
+ incoming.actorTrust.memberType === "external") {
92
+ throw new IntegrationIdentityDeclinedError("guest", "External or guest Slack members cannot use this integration.", "This assistant is only available to members of this workspace's organization.");
79
93
  }
80
94
  if (!installation?.orgId) {
81
- throw new Error("Slack workspace is not connected to an Agent Native organization.");
95
+ throw new IntegrationIdentityDeclinedError("unlinked-workspace", "Slack workspace is not connected to an Agent Native organization.", "This Slack workspace isn't connected to an organization yet.");
82
96
  }
83
- if (!(await isMemberOfOrg(email, installation.orgId))) {
84
- throw new Error("Slack DM sender is not a member of the connected Agent Native organization.");
97
+ const email = normalizedEmail(incoming.senderEmail);
98
+ let isOrgMember = false;
99
+ if (email &&
100
+ incoming.senderVerified === true &&
101
+ incoming.senderId &&
102
+ incoming.tenantId) {
103
+ try {
104
+ isOrgMember = await isMemberOfOrg(email, installation.orgId);
105
+ }
106
+ catch {
107
+ // A membership-store outage is not evidence that the sender is merely
108
+ // unlinked. Fail closed instead of widening them to org-wide access.
109
+ throw new IntegrationIdentityDeclinedError("membership-check-failed", "Slack DM organization membership could not be verified.", "I couldn't verify your organization membership just now, so I can't run this request. Please try again in a moment.");
110
+ }
85
111
  }
86
- const link = await upsertVerifiedIntegrationIdentity({
87
- platform: incoming.platform,
88
- tenantId: incoming.tenantId,
89
- externalUserId: incoming.senderId,
90
- userEmail: email,
91
- orgId: installation.orgId,
92
- });
112
+ if (isOrgMember && email && incoming.senderId && incoming.tenantId) {
113
+ const link = await upsertVerifiedIntegrationIdentity({
114
+ platform: incoming.platform,
115
+ tenantId: incoming.tenantId,
116
+ externalUserId: incoming.senderId,
117
+ userEmail: email,
118
+ orgId: installation.orgId,
119
+ });
120
+ return {
121
+ ownerEmail: link.userEmail,
122
+ orgId: link.orgId,
123
+ principalType: "user",
124
+ installationId: installation.id,
125
+ };
126
+ }
127
+ // Hydrated full workspace member (member/admin/owner) whose email is
128
+ // unverified, missing (legacy install without users:read.email), or not an
129
+ // organization member: run with the anonymous org-scoped service principal —
130
+ // the same visibility shared channels get. Nothing user-private is
131
+ // accessible. One structured line keeps 100%-anonymous workspaces (legacy
132
+ // scope missing) visible in logs.
133
+ console.warn(`[integrations] anonymous org-scoped principal used: platform=${incoming.platform} teamId=${incoming.tenantId ?? "unknown"} emailPresent=${Boolean(email)} memberType=${incoming.actorTrust.memberType}`);
93
134
  return {
94
- ownerEmail: link.userEmail,
95
- orgId: link.orgId,
96
- principalType: "user",
135
+ ownerEmail: serviceOwner(incoming.platform),
136
+ orgId: installation.orgId,
137
+ principalType: "service",
97
138
  installationId: installation.id,
139
+ anonymousMember: true,
98
140
  };
99
141
  }
100
142
  //# sourceMappingURL=identity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/integrations/identity.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec } from \"../db/client.js\";\nimport { upsertVerifiedIntegrationIdentity } from \"./identity-links-store.js\";\nimport {\n getActiveIntegrationInstallationByKey,\n getActiveIntegrationInstallationForTenant,\n} from \"./installations-store.js\";\nimport { slackInstallationKey } from \"./slack-oauth.js\";\nimport type { IncomingMessage, IntegrationExecutionContext } from \"./types.js\";\n\nfunction serviceOwner(platform: string): string {\n return `integration@${platform}`;\n}\n\nfunction normalizedEmail(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const email = value.trim().toLowerCase();\n if (!email || email.length > 320 || !email.includes(\"@\")) return null;\n return email;\n}\n\nasync function resolveSlackInstallation(incoming: IncomingMessage) {\n const teamId =\n typeof incoming.platformContext.teamId === \"string\"\n ? incoming.platformContext.teamId\n : incoming.tenantId;\n const enterpriseId =\n typeof incoming.platformContext.enterpriseId === \"string\"\n ? incoming.platformContext.enterpriseId\n : undefined;\n const apiAppId =\n typeof incoming.platformContext.apiAppId === \"string\"\n ? incoming.platformContext.apiAppId\n : undefined;\n if (apiAppId && (teamId || enterpriseId)) {\n const byKey = await getActiveIntegrationInstallationByKey(\n \"slack\",\n slackInstallationKey({ teamId, enterpriseId, apiAppId }),\n );\n if (byKey) return byKey;\n }\n return teamId || enterpriseId\n ? getActiveIntegrationInstallationForTenant(\n \"slack\",\n teamId ?? enterpriseId!,\n )\n : null;\n}\n\nasync function isMemberOfOrg(email: string, orgId: string): Promise<boolean> {\n try {\n const { rows } = await getDbExec().execute({\n sql: `SELECT 1 FROM org_members\n WHERE org_id = ? AND LOWER(email) = ?\n LIMIT 1`,\n args: [orgId, email],\n });\n return rows.length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the default integration principal.\n *\n * Slack DMs become a user principal only after the adapter has verified the\n * sender email and the email is already a member of the managed installation's\n * Agent Native organization. Shared channels deliberately stay service-scoped\n * so a channel message cannot borrow one participant's private permissions.\n */\nexport async function resolveDefaultIntegrationExecutionContext(\n incoming: IncomingMessage,\n): Promise<IntegrationExecutionContext> {\n const installation =\n incoming.platform === \"slack\"\n ? await resolveSlackInstallation(incoming)\n : null;\n\n if (incoming.platform !== \"slack\" || incoming.conversationType !== \"dm\") {\n return {\n ownerEmail: serviceOwner(incoming.platform),\n orgId: installation?.orgId ?? null,\n principalType: \"service\",\n ...(installation?.id ? { installationId: installation.id } : {}),\n };\n }\n\n const email = normalizedEmail(incoming.senderEmail);\n if (\n !email ||\n incoming.senderVerified !== true ||\n !incoming.senderId ||\n !incoming.tenantId\n ) {\n throw new Error(\n \"Slack DM sender identity could not be verified; refusing to run with a service principal.\",\n );\n }\n if (\n incoming.actorTrust?.memberType === \"guest\" ||\n incoming.actorTrust?.memberType === \"external\"\n ) {\n throw new Error(\n \"External or guest Slack members cannot use personal Agent Native permissions.\",\n );\n }\n if (!installation?.orgId) {\n throw new Error(\n \"Slack workspace is not connected to an Agent Native organization.\",\n );\n }\n\n if (!(await isMemberOfOrg(email, installation.orgId))) {\n throw new Error(\n \"Slack DM sender is not a member of the connected Agent Native organization.\",\n );\n }\n\n const link = await upsertVerifiedIntegrationIdentity({\n platform: incoming.platform,\n tenantId: incoming.tenantId,\n externalUserId: incoming.senderId,\n userEmail: email,\n orgId: installation.orgId,\n });\n return {\n ownerEmail: link.userEmail,\n orgId: link.orgId,\n principalType: \"user\",\n installationId: installation.id,\n };\n}\n"]}
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/integrations/identity.ts"],"names":[],"mappings":"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","sourcesContent":["import { getDbExec } from \"../db/client.js\";\nimport { upsertVerifiedIntegrationIdentity } from \"./identity-links-store.js\";\nimport {\n getActiveIntegrationInstallationByKey,\n getActiveIntegrationInstallationForTenant,\n} from \"./installations-store.js\";\nimport { slackInstallationKey } from \"./slack-oauth.js\";\nimport type { IncomingMessage, IntegrationExecutionContext } from \"./types.js\";\n\nexport type IntegrationIdentityDeclineReason =\n | \"unverified\"\n | \"guest\"\n | \"unlinked-workspace\"\n | \"membership-check-failed\";\n\n/**\n * Thrown when the default Slack DM identity ladder declines to run a message.\n * `reason` is a stable machine-readable discriminator (used e.g. to dedupe\n * decline replies); `userFacingMessage` is safe to send back to the sender as\n * a polite reply; `message` stays log-only.\n */\nexport class IntegrationIdentityDeclinedError extends Error {\n readonly reason: IntegrationIdentityDeclineReason;\n readonly userFacingMessage: string;\n\n constructor(\n reason: IntegrationIdentityDeclineReason,\n message: string,\n userFacingMessage: string,\n ) {\n super(message);\n this.name = \"IntegrationIdentityDeclinedError\";\n this.reason = reason;\n this.userFacingMessage = userFacingMessage;\n }\n}\n\nfunction serviceOwner(platform: string): string {\n return `integration@${platform}`;\n}\n\nfunction normalizedEmail(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const email = value.trim().toLowerCase();\n if (!email || email.length > 320 || !email.includes(\"@\")) return null;\n return email;\n}\n\nasync function resolveSlackInstallation(incoming: IncomingMessage) {\n const teamId =\n typeof incoming.platformContext.teamId === \"string\"\n ? incoming.platformContext.teamId\n : incoming.tenantId;\n const enterpriseId =\n typeof incoming.platformContext.enterpriseId === \"string\"\n ? incoming.platformContext.enterpriseId\n : undefined;\n const apiAppId =\n typeof incoming.platformContext.apiAppId === \"string\"\n ? incoming.platformContext.apiAppId\n : undefined;\n if (apiAppId && (teamId || enterpriseId)) {\n const byKey = await getActiveIntegrationInstallationByKey(\n \"slack\",\n slackInstallationKey({ teamId, enterpriseId, apiAppId }),\n );\n if (byKey) return byKey;\n }\n return teamId || enterpriseId\n ? getActiveIntegrationInstallationForTenant(\n \"slack\",\n teamId ?? enterpriseId!,\n )\n : null;\n}\n\nasync function isMemberOfOrg(email: string, orgId: string): Promise<boolean> {\n const { rows } = await getDbExec().execute({\n sql: `SELECT 1 FROM org_members\n WHERE org_id = ? AND LOWER(email) = ?\n LIMIT 1`,\n args: [orgId, email],\n });\n return rows.length > 0;\n}\n\n/**\n * Resolve the default integration principal.\n *\n * Slack DMs become a user principal only after the adapter has verified the\n * sender email and the email is already a member of the managed installation's\n * Agent Native organization. Hydrated full workspace members whose email is\n * missing, unverified, or not an organization member run as an anonymous\n * org-scoped service principal instead — the same visibility shared channels\n * get. Hydration failures, guests/external members, and workspaces without a\n * connected organization are declined with a user-facing message. Shared\n * channels deliberately stay service-scoped so a channel message cannot borrow\n * one participant's private permissions.\n */\nexport async function resolveDefaultIntegrationExecutionContext(\n incoming: IncomingMessage,\n): Promise<IntegrationExecutionContext> {\n const installation =\n incoming.platform === \"slack\"\n ? await resolveSlackInstallation(incoming)\n : null;\n\n if (incoming.platform !== \"slack\" || incoming.conversationType !== \"dm\") {\n return {\n ownerEmail: serviceOwner(incoming.platform),\n orgId: installation?.orgId ?? null,\n principalType: \"service\",\n ...(installation?.id ? { installationId: installation.id } : {}),\n };\n }\n\n // Hydration check first: a transient users.info failure must land on the\n // retry decline here, never on the anonymous org-scoped tier below.\n if (incoming.actorTrust?.verified !== true) {\n throw new IntegrationIdentityDeclinedError(\n \"unverified\",\n \"Slack DM sender identity could not be hydrated; declining instead of guessing a principal.\",\n \"I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.\",\n );\n }\n if (\n incoming.actorTrust.memberType === \"guest\" ||\n incoming.actorTrust.memberType === \"external\"\n ) {\n throw new IntegrationIdentityDeclinedError(\n \"guest\",\n \"External or guest Slack members cannot use this integration.\",\n \"This assistant is only available to members of this workspace's organization.\",\n );\n }\n if (!installation?.orgId) {\n throw new IntegrationIdentityDeclinedError(\n \"unlinked-workspace\",\n \"Slack workspace is not connected to an Agent Native organization.\",\n \"This Slack workspace isn't connected to an organization yet.\",\n );\n }\n\n const email = normalizedEmail(incoming.senderEmail);\n let isOrgMember = false;\n if (\n email &&\n incoming.senderVerified === true &&\n incoming.senderId &&\n incoming.tenantId\n ) {\n try {\n isOrgMember = await isMemberOfOrg(email, installation.orgId);\n } catch {\n // A membership-store outage is not evidence that the sender is merely\n // unlinked. Fail closed instead of widening them to org-wide access.\n throw new IntegrationIdentityDeclinedError(\n \"membership-check-failed\",\n \"Slack DM organization membership could not be verified.\",\n \"I couldn't verify your organization membership just now, so I can't run this request. Please try again in a moment.\",\n );\n }\n }\n if (isOrgMember && email && incoming.senderId && incoming.tenantId) {\n const link = await upsertVerifiedIntegrationIdentity({\n platform: incoming.platform,\n tenantId: incoming.tenantId,\n externalUserId: incoming.senderId,\n userEmail: email,\n orgId: installation.orgId,\n });\n return {\n ownerEmail: link.userEmail,\n orgId: link.orgId,\n principalType: \"user\",\n installationId: installation.id,\n };\n }\n\n // Hydrated full workspace member (member/admin/owner) whose email is\n // unverified, missing (legacy install without users:read.email), or not an\n // organization member: run with the anonymous org-scoped service principal —\n // the same visibility shared channels get. Nothing user-private is\n // accessible. One structured line keeps 100%-anonymous workspaces (legacy\n // scope missing) visible in logs.\n console.warn(\n `[integrations] anonymous org-scoped principal used: platform=${incoming.platform} teamId=${incoming.tenantId ?? \"unknown\"} emailPresent=${Boolean(email)} memberType=${incoming.actorTrust.memberType}`,\n );\n return {\n ownerEmail: serviceOwner(incoming.platform),\n orgId: installation.orgId,\n principalType: \"service\",\n installationId: installation.id,\n anonymousMember: true,\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/integrations/plugin.ts"],"names":[],"mappings":"AAsIA,OAAO,KAAK,EAEV,yBAAyB,EAG1B,MAAM,YAAY,CAAC;AAOpB,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AA4F9D,KAAK,yBAAyB,GAAG;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAUF,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,yBAAyB,GAClC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsFlC;AA+ND;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,CAAC,EAAE,yBAAyB,GAClC,cAAc,CA69DhB;AAED;;GAEG;AACH,eAAO,MAAM,yBAAyB,gBAA6B,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/integrations/plugin.ts"],"names":[],"mappings":"AAyIA,OAAO,KAAK,EAEV,yBAAyB,EAG1B,MAAM,YAAY,CAAC;AAOpB,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AA4F9D,KAAK,yBAAyB,GAAG;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAeF,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,yBAAyB,GAClC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsFlC;AA+ND;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,CAAC,EAAE,yBAAyB,GAClC,cAAc,CA6hEhB;AAED;;GAEG;AACH,eAAO,MAAM,yBAAyB,gBAA6B,CAAC"}
@@ -26,7 +26,7 @@ import { ComputerSupervisionError } from "./computer-supervision.js";
26
26
  import { getIntegrationConfig, saveIntegrationConfig } from "./config-store.js";
27
27
  import { claimIntegrationControl } from "./controls-store.js";
28
28
  import { startGoogleDocsPoller, handlePushNotification, } from "./google-docs-poller.js";
29
- import { resolveDefaultIntegrationExecutionContext } from "./identity.js";
29
+ import { IntegrationIdentityDeclinedError, resolveDefaultIntegrationExecutionContext, } from "./identity.js";
30
30
  import { disconnectIntegrationInstallation, listIntegrationInstallations, resolveIntegrationTokenBundle, updateIntegrationInstallation, upsertIntegrationInstallation, } from "./installations-store.js";
31
31
  import { forgetIntegrationMemory, integrationMemoryActions, listIntegrationMemory, rememberForIntegrationScope, } from "./integration-memory.js";
32
32
  import { extractBearerToken, signInternalToken, verifyInternalToken, } from "./internal-token.js";
@@ -124,6 +124,10 @@ Keep responses concise — messaging platforms have character limits and users e
124
124
 
125
125
  If a task requires many steps, summarize what you did rather than streaming every detail.`;
126
126
  const REMOTE_DEVICE_ONLINE_MS = 90_000;
127
+ // One decline reply per sender + decline reason per window: during a Slack
128
+ // API outage every message would otherwise get another identical "try again"
129
+ // reply. Short enough that a persistent condition still reminds the sender.
130
+ const DECLINE_NOTICE_DEDUPE_TTL_MS = 5 * 60 * 1_000;
127
131
  export async function enqueueRemoteCommand(envelope) {
128
132
  const ownerEmail = readString(envelope.ownerEmail);
129
133
  if (!ownerEmail)
@@ -1912,11 +1916,33 @@ export function createIntegrationsPlugin(options) {
1912
1916
  defaultExecutionContext = await withCredentialContext(credentialContext, () => resolveDefaultIntegrationExecutionContext(incoming));
1913
1917
  }
1914
1918
  catch (err) {
1915
- // An app-specific resolver may intentionally support a legacy
1916
- // Slack setup. When no override exists, the branch below fails
1917
- // closed instead of falling back to a service principal.
1918
- if (!options?.resolveExecutionContext && !options?.resolveOwner) {
1919
- console.error(`[integrations] default Slack DM identity denied message:`, err);
1919
+ // Only an explicit execution-context resolver may override the
1920
+ // default Slack identity ladder. The legacy owner-only resolver
1921
+ // predates org-bound identities and must not turn a rejected DM
1922
+ // into an authenticated owner run.
1923
+ if (!options?.resolveExecutionContext) {
1924
+ const declined = err instanceof IntegrationIdentityDeclinedError ? err : null;
1925
+ if (declined) {
1926
+ console.warn(`[integrations] default Slack DM identity declined message:`, declined.message);
1927
+ // Best-effort polite reply back to the DM, deduped per
1928
+ // sender + reason so an outage doesn't produce an identical
1929
+ // reply for every message. Do not await Slack's API: webhook
1930
+ // acknowledgement has a stricter deadline than notice
1931
+ // delivery and must not be delayed by a slow postMessage.
1932
+ void withCredentialContext(credentialContext, () => adapter.sendSystemNotice
1933
+ ? adapter.sendSystemNotice(incoming, declined.userFacingMessage, {
1934
+ dedupeKey: `decline:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}:${declined.reason}`,
1935
+ dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
1936
+ })
1937
+ : Promise.resolve()).catch((noticeErr) => {
1938
+ console.warn(`[integrations] decline notice failed:`, noticeErr instanceof Error
1939
+ ? noticeErr.message
1940
+ : noticeErr);
1941
+ });
1942
+ }
1943
+ else {
1944
+ console.error(`[integrations] default Slack DM identity denied message:`, err);
1945
+ }
1920
1946
  setResponseStatus(event, 200);
1921
1947
  return "ok";
1922
1948
  }
@@ -1939,6 +1965,30 @@ export function createIntegrationsPlugin(options) {
1939
1965
  }
1940
1966
  else if (defaultExecutionContext) {
1941
1967
  executionContext = defaultExecutionContext;
1968
+ if (defaultExecutionContext.anonymousMember) {
1969
+ // The anonymous tier must never be silent. (1) The agent run
1970
+ // can tell: the note rides the serialized `incoming` into the
1971
+ // queued task and surfaces via <integration-context>.
1972
+ incoming.identityNote =
1973
+ "Caller is an unlinked Slack workspace member running with organization-wide visibility only; personal or privately-shared data is not accessible. They can get personal access by having an admin add their Slack email to the organization (or by reconnecting Slack with the users:read.email scope).";
1974
+ // (2) The sender gets a one-time heads-up (adapter-throttled per
1975
+ // sender). Fire-and-forget so it never delays or replaces the
1976
+ // normal agent run.
1977
+ if (adapter.sendSystemNotice) {
1978
+ const senderEmail = typeof incoming.senderEmail === "string" &&
1979
+ incoming.senderEmail.trim()
1980
+ ? incoming.senderEmail.trim()
1981
+ : null;
1982
+ const noticeText = senderEmail
1983
+ ? `Heads up: I couldn't match your Slack account to an organization member, so I can only use org-wide data. Ask an admin to add ${senderEmail} to the organization for personal access.`
1984
+ : "Heads up: I couldn't verify your Slack account's email, so I can only use org-wide data. Ask an admin to update the Slack connection with the users:read.email scope for personal access.";
1985
+ void withCredentialContext(credentialContext, () => adapter.sendSystemNotice(incoming, noticeText, {
1986
+ dedupeKey: `anonymous-tier:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
1987
+ })).catch((noticeErr) => {
1988
+ console.warn(`[integrations] anonymous-tier notice failed:`, noticeErr instanceof Error ? noticeErr.message : noticeErr);
1989
+ });
1990
+ }
1991
+ }
1942
1992
  }
1943
1993
  else if (options?.resolveOwner) {
1944
1994
  try {