@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
@@ -56,6 +56,31 @@ const slackIdentityCache = new Map<
56
56
  { identity: SlackUserIdentity | null; expiresAt: number }
57
57
  >();
58
58
 
59
+ // Deduped system notices send at most once per key per TTL so senders are
60
+ // informed without being spammed per message. Callers pick the window: the
61
+ // anonymous-tier heads-up uses the default day-long TTL; decline replies pass
62
+ // a short TTL so a persistent condition still reminds the sender occasionally.
63
+ const SLACK_SYSTEM_NOTICE_DEDUPE_TTL_MS = 24 * 60 * 60 * 1_000;
64
+ const SLACK_SYSTEM_NOTICE_CACHE_MAX_ENTRIES = 1_000;
65
+ const slackSystemNoticeCache = new Map<string, number>();
66
+
67
+ /** Returns true when a deduped notice should send now, claiming the slot. */
68
+ function claimSlackSystemNoticeSlot(
69
+ key: string,
70
+ ttlMs = SLACK_SYSTEM_NOTICE_DEDUPE_TTL_MS,
71
+ ): boolean {
72
+ const now = Date.now();
73
+ const expiresAt = slackSystemNoticeCache.get(key);
74
+ if (expiresAt && expiresAt > now) return false;
75
+ if (expiresAt) slackSystemNoticeCache.delete(key);
76
+ if (slackSystemNoticeCache.size >= SLACK_SYSTEM_NOTICE_CACHE_MAX_ENTRIES) {
77
+ const oldestKey = slackSystemNoticeCache.keys().next().value;
78
+ if (oldestKey) slackSystemNoticeCache.delete(oldestKey);
79
+ }
80
+ slackSystemNoticeCache.set(key, now + ttlMs);
81
+ return true;
82
+ }
83
+
59
84
  export interface SlackAdapterOptions {
60
85
  /** Resolve the bot token for the exact Slack installation. */
61
86
  resolveBotToken?: (incoming: IncomingMessage) => Promise<string | undefined>;
@@ -264,7 +289,10 @@ export function slackAdapter(
264
289
  triggerKind: isDm ? "dm" : isMention ? "mention" : "thread_reply",
265
290
  conversationType: isDm ? "dm" : "unknown",
266
291
  tenantId: teamId,
267
- actorTrust: { memberType: "unknown", verified: true },
292
+ // The signed Slack envelope authenticates the workspace, not the
293
+ // sender's membership tier. `users.info` hydration below is the
294
+ // authority that may promote this to a verified member.
295
+ actorTrust: { memberType: "unknown", verified: false },
268
296
  platformContext: {
269
297
  channelId: e.channel,
270
298
  channelType: e.channel_type,
@@ -493,6 +521,48 @@ export function slackAdapter(
493
521
  }
494
522
  },
495
523
 
524
+ async sendSystemNotice(
525
+ incoming: IncomingMessage,
526
+ text: string,
527
+ opts?: { dedupeKey?: string; dedupeTtlMs?: number },
528
+ ): Promise<void> {
529
+ if (!text.trim()) return;
530
+ const dedupeKey = opts?.dedupeKey;
531
+ if (
532
+ dedupeKey &&
533
+ !claimSlackSystemNoticeSlot(dedupeKey, opts?.dedupeTtlMs)
534
+ ) {
535
+ return;
536
+ }
537
+ try {
538
+ const token = await resolveBotToken(incoming);
539
+ if (!token) {
540
+ if (dedupeKey) slackSystemNoticeCache.delete(dedupeKey);
541
+ return;
542
+ }
543
+ const channelId = incoming.platformContext.channelId;
544
+ if (typeof channelId !== "string" || !channelId) {
545
+ if (dedupeKey) slackSystemNoticeCache.delete(dedupeKey);
546
+ return;
547
+ }
548
+ const threadTs =
549
+ typeof incoming.platformContext.threadTs === "string"
550
+ ? incoming.platformContext.threadTs
551
+ : undefined;
552
+ await postFresh(token, channelId, threadTs, {
553
+ text,
554
+ unfurl_links: false,
555
+ unfurl_media: false,
556
+ mrkdwn: true,
557
+ });
558
+ } catch (error) {
559
+ // A failed delivery did not inform the sender, so release the slot and
560
+ // allow the next message to retry instead of suppressing notices for a day.
561
+ if (dedupeKey) slackSystemNoticeCache.delete(dedupeKey);
562
+ throw error;
563
+ }
564
+ },
565
+
496
566
  async sendMessageToTarget(
497
567
  message: OutgoingMessage,
498
568
  target: OutboundTarget,
@@ -1137,15 +1207,18 @@ async function resolveSlackUserIdentity(
1137
1207
  : typeof user.user.name === "string" && user.user.name.trim()
1138
1208
  ? user.user.name.trim()
1139
1209
  : null,
1140
- memberType: user.user.is_ultra_restricted
1141
- ? "external"
1142
- : user.user.is_restricted
1143
- ? "guest"
1144
- : user.user.is_owner
1145
- ? "owner"
1146
- : user.user.is_admin
1147
- ? "admin"
1148
- : "member",
1210
+ // is_stranger marks Slack Connect DM participants from another
1211
+ // workspace; they must map to "external", never "member".
1212
+ memberType:
1213
+ user.user.is_stranger || user.user.is_ultra_restricted
1214
+ ? "external"
1215
+ : user.user.is_restricted
1216
+ ? "guest"
1217
+ : user.user.is_owner
1218
+ ? "owner"
1219
+ : user.user.is_admin
1220
+ ? "admin"
1221
+ : "member",
1149
1222
  }
1150
1223
  : null;
1151
1224
  if (slackIdentityCache.size >= SLACK_IDENTITY_CACHE_MAX_ENTRIES) {
@@ -1168,7 +1241,13 @@ async function hydrateSlackIdentity(
1168
1241
  incoming: IncomingMessage,
1169
1242
  ): Promise<IncomingMessage> {
1170
1243
  const identity = await resolveSlackUserIdentity(token, incoming);
1171
- if (!identity) return incoming;
1244
+ if (!identity) {
1245
+ return {
1246
+ ...incoming,
1247
+ senderVerified: false,
1248
+ actorTrust: { memberType: "unknown", verified: false },
1249
+ };
1250
+ }
1172
1251
  return {
1173
1252
  ...incoming,
1174
1253
  ...(identity.name ? { senderName: identity.name } : {}),
@@ -1299,8 +1378,6 @@ async function hydrateSlackContext(
1299
1378
  : [];
1300
1379
 
1301
1380
  const profile = identity;
1302
- const isGuest = identity?.memberType === "guest";
1303
- const isExternal = identity?.memberType === "external";
1304
1381
  const isPrivate = conversation?.channel?.is_private === true;
1305
1382
  const isExternalShared = conversation?.channel?.is_ext_shared === true;
1306
1383
  const isMpim = conversation?.channel?.is_mpim === true;
@@ -1348,12 +1425,15 @@ async function hydrateSlackContext(
1348
1425
  ? { senderEmail: profile.email, senderVerified: true }
1349
1426
  : { senderVerified: false }),
1350
1427
  conversationType,
1351
- actorTrust: {
1352
- memberType:
1353
- identity?.memberType ??
1354
- (isExternal ? "external" : isGuest ? "guest" : "member"),
1355
- verified: true,
1356
- },
1428
+ // Conversation hydration must not promote a caller when users.info was
1429
+ // unavailable. Preserve the result of the earlier identity-only hydration
1430
+ // unless this request independently resolved the Slack user.
1431
+ actorTrust: identity
1432
+ ? { memberType: identity.memberType, verified: true }
1433
+ : (incoming.actorTrust ?? {
1434
+ memberType: "unknown",
1435
+ verified: false,
1436
+ }),
1357
1437
  contextMessages: hydratedMessages,
1358
1438
  ...(hydratedFiles.length ? { files: hydratedFiles.slice(0, 20) } : {}),
1359
1439
  platformContext: {
@@ -7,6 +7,34 @@ import {
7
7
  import { slackInstallationKey } from "./slack-oauth.js";
8
8
  import type { IncomingMessage, IntegrationExecutionContext } from "./types.js";
9
9
 
10
+ export type IntegrationIdentityDeclineReason =
11
+ | "unverified"
12
+ | "guest"
13
+ | "unlinked-workspace"
14
+ | "membership-check-failed";
15
+
16
+ /**
17
+ * Thrown when the default Slack DM identity ladder declines to run a message.
18
+ * `reason` is a stable machine-readable discriminator (used e.g. to dedupe
19
+ * decline replies); `userFacingMessage` is safe to send back to the sender as
20
+ * a polite reply; `message` stays log-only.
21
+ */
22
+ export class IntegrationIdentityDeclinedError extends Error {
23
+ readonly reason: IntegrationIdentityDeclineReason;
24
+ readonly userFacingMessage: string;
25
+
26
+ constructor(
27
+ reason: IntegrationIdentityDeclineReason,
28
+ message: string,
29
+ userFacingMessage: string,
30
+ ) {
31
+ super(message);
32
+ this.name = "IntegrationIdentityDeclinedError";
33
+ this.reason = reason;
34
+ this.userFacingMessage = userFacingMessage;
35
+ }
36
+ }
37
+
10
38
  function serviceOwner(platform: string): string {
11
39
  return `integration@${platform}`;
12
40
  }
@@ -47,17 +75,13 @@ async function resolveSlackInstallation(incoming: IncomingMessage) {
47
75
  }
48
76
 
49
77
  async function isMemberOfOrg(email: string, orgId: string): Promise<boolean> {
50
- try {
51
- const { rows } = await getDbExec().execute({
52
- sql: `SELECT 1 FROM org_members
78
+ const { rows } = await getDbExec().execute({
79
+ sql: `SELECT 1 FROM org_members
53
80
  WHERE org_id = ? AND LOWER(email) = ?
54
81
  LIMIT 1`,
55
- args: [orgId, email],
56
- });
57
- return rows.length > 0;
58
- } catch {
59
- return false;
60
- }
82
+ args: [orgId, email],
83
+ });
84
+ return rows.length > 0;
61
85
  }
62
86
 
63
87
  /**
@@ -65,8 +89,13 @@ async function isMemberOfOrg(email: string, orgId: string): Promise<boolean> {
65
89
  *
66
90
  * Slack DMs become a user principal only after the adapter has verified the
67
91
  * 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.
92
+ * Agent Native organization. Hydrated full workspace members whose email is
93
+ * missing, unverified, or not an organization member run as an anonymous
94
+ * org-scoped service principal instead — the same visibility shared channels
95
+ * get. Hydration failures, guests/external members, and workspaces without a
96
+ * connected organization are declined with a user-facing message. Shared
97
+ * channels deliberately stay service-scoped so a channel message cannot borrow
98
+ * one participant's private permissions.
70
99
  */
71
100
  export async function resolveDefaultIntegrationExecutionContext(
72
101
  incoming: IncomingMessage,
@@ -85,48 +114,83 @@ export async function resolveDefaultIntegrationExecutionContext(
85
114
  };
86
115
  }
87
116
 
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.",
117
+ // Hydration check first: a transient users.info failure must land on the
118
+ // retry decline here, never on the anonymous org-scoped tier below.
119
+ if (incoming.actorTrust?.verified !== true) {
120
+ throw new IntegrationIdentityDeclinedError(
121
+ "unverified",
122
+ "Slack DM sender identity could not be hydrated; declining instead of guessing a principal.",
123
+ "I couldn't verify your Slack identity just now, so I can't run this request. Please try again in a moment.",
97
124
  );
98
125
  }
99
126
  if (
100
- incoming.actorTrust?.memberType === "guest" ||
101
- incoming.actorTrust?.memberType === "external"
127
+ incoming.actorTrust.memberType === "guest" ||
128
+ incoming.actorTrust.memberType === "external"
102
129
  ) {
103
- throw new Error(
104
- "External or guest Slack members cannot use personal Agent Native permissions.",
130
+ throw new IntegrationIdentityDeclinedError(
131
+ "guest",
132
+ "External or guest Slack members cannot use this integration.",
133
+ "This assistant is only available to members of this workspace's organization.",
105
134
  );
106
135
  }
107
136
  if (!installation?.orgId) {
108
- throw new Error(
137
+ throw new IntegrationIdentityDeclinedError(
138
+ "unlinked-workspace",
109
139
  "Slack workspace is not connected to an Agent Native organization.",
140
+ "This Slack workspace isn't connected to an organization yet.",
110
141
  );
111
142
  }
112
143
 
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
- );
144
+ const email = normalizedEmail(incoming.senderEmail);
145
+ let isOrgMember = false;
146
+ if (
147
+ email &&
148
+ incoming.senderVerified === true &&
149
+ incoming.senderId &&
150
+ incoming.tenantId
151
+ ) {
152
+ try {
153
+ isOrgMember = await isMemberOfOrg(email, installation.orgId);
154
+ } catch {
155
+ // A membership-store outage is not evidence that the sender is merely
156
+ // unlinked. Fail closed instead of widening them to org-wide access.
157
+ throw new IntegrationIdentityDeclinedError(
158
+ "membership-check-failed",
159
+ "Slack DM organization membership could not be verified.",
160
+ "I couldn't verify your organization membership just now, so I can't run this request. Please try again in a moment.",
161
+ );
162
+ }
163
+ }
164
+ if (isOrgMember && email && incoming.senderId && incoming.tenantId) {
165
+ const link = await upsertVerifiedIntegrationIdentity({
166
+ platform: incoming.platform,
167
+ tenantId: incoming.tenantId,
168
+ externalUserId: incoming.senderId,
169
+ userEmail: email,
170
+ orgId: installation.orgId,
171
+ });
172
+ return {
173
+ ownerEmail: link.userEmail,
174
+ orgId: link.orgId,
175
+ principalType: "user",
176
+ installationId: installation.id,
177
+ };
117
178
  }
118
179
 
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
- });
180
+ // Hydrated full workspace member (member/admin/owner) whose email is
181
+ // unverified, missing (legacy install without users:read.email), or not an
182
+ // organization member: run with the anonymous org-scoped service principal —
183
+ // the same visibility shared channels get. Nothing user-private is
184
+ // accessible. One structured line keeps 100%-anonymous workspaces (legacy
185
+ // scope missing) visible in logs.
186
+ console.warn(
187
+ `[integrations] anonymous org-scoped principal used: platform=${incoming.platform} teamId=${incoming.tenantId ?? "unknown"} emailPresent=${Boolean(email)} memberType=${incoming.actorTrust.memberType}`,
188
+ );
126
189
  return {
127
- ownerEmail: link.userEmail,
128
- orgId: link.orgId,
129
- principalType: "user",
190
+ ownerEmail: serviceOwner(incoming.platform),
191
+ orgId: installation.orgId,
192
+ principalType: "service",
130
193
  installationId: installation.id,
194
+ anonymousMember: true,
131
195
  };
132
196
  }
@@ -53,7 +53,10 @@ import {
53
53
  startGoogleDocsPoller,
54
54
  handlePushNotification,
55
55
  } from "./google-docs-poller.js";
56
- import { resolveDefaultIntegrationExecutionContext } from "./identity.js";
56
+ import {
57
+ IntegrationIdentityDeclinedError,
58
+ resolveDefaultIntegrationExecutionContext,
59
+ } from "./identity.js";
57
60
  import {
58
61
  disconnectIntegrationInstallation,
59
62
  listIntegrationInstallations,
@@ -252,6 +255,11 @@ type IntegrationCredentialContext = {
252
255
 
253
256
  const REMOTE_DEVICE_ONLINE_MS = 90_000;
254
257
 
258
+ // One decline reply per sender + decline reason per window: during a Slack
259
+ // API outage every message would otherwise get another identical "try again"
260
+ // reply. Short enough that a persistent condition still reminds the sender.
261
+ const DECLINE_NOTICE_DEDUPE_TTL_MS = 5 * 60 * 1_000;
262
+
255
263
  export async function enqueueRemoteCommand(
256
264
  envelope: RemoteCodeCommandEnvelope,
257
265
  ): Promise<Record<string, unknown>> {
@@ -2378,14 +2386,48 @@ export function createIntegrationsPlugin(
2378
2386
  () => resolveDefaultIntegrationExecutionContext(incoming!),
2379
2387
  );
2380
2388
  } 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
+ // Only an explicit execution-context resolver may override the
2390
+ // default Slack identity ladder. The legacy owner-only resolver
2391
+ // predates org-bound identities and must not turn a rejected DM
2392
+ // into an authenticated owner run.
2393
+ if (!options?.resolveExecutionContext) {
2394
+ const declined =
2395
+ err instanceof IntegrationIdentityDeclinedError ? err : null;
2396
+ if (declined) {
2397
+ console.warn(
2398
+ `[integrations] default Slack DM identity declined message:`,
2399
+ declined.message,
2400
+ );
2401
+ // Best-effort polite reply back to the DM, deduped per
2402
+ // sender + reason so an outage doesn't produce an identical
2403
+ // reply for every message. Do not await Slack's API: webhook
2404
+ // acknowledgement has a stricter deadline than notice
2405
+ // delivery and must not be delayed by a slow postMessage.
2406
+ void withCredentialContext(credentialContext, () =>
2407
+ adapter.sendSystemNotice
2408
+ ? adapter.sendSystemNotice(
2409
+ incoming!,
2410
+ declined.userFacingMessage,
2411
+ {
2412
+ dedupeKey: `decline:${incoming!.tenantId ?? "unknown"}:${incoming!.senderId ?? "unknown"}:${declined.reason}`,
2413
+ dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
2414
+ },
2415
+ )
2416
+ : Promise.resolve(),
2417
+ ).catch((noticeErr) => {
2418
+ console.warn(
2419
+ `[integrations] decline notice failed:`,
2420
+ noticeErr instanceof Error
2421
+ ? noticeErr.message
2422
+ : noticeErr,
2423
+ );
2424
+ });
2425
+ } else {
2426
+ console.error(
2427
+ `[integrations] default Slack DM identity denied message:`,
2428
+ err,
2429
+ );
2430
+ }
2389
2431
  setResponseStatus(event, 200);
2390
2432
  return "ok";
2391
2433
  }
@@ -2413,6 +2455,36 @@ export function createIntegrationsPlugin(
2413
2455
  }
2414
2456
  } else if (defaultExecutionContext) {
2415
2457
  executionContext = defaultExecutionContext;
2458
+ if (defaultExecutionContext.anonymousMember) {
2459
+ // The anonymous tier must never be silent. (1) The agent run
2460
+ // can tell: the note rides the serialized `incoming` into the
2461
+ // queued task and surfaces via <integration-context>.
2462
+ incoming.identityNote =
2463
+ "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).";
2464
+ // (2) The sender gets a one-time heads-up (adapter-throttled per
2465
+ // sender). Fire-and-forget so it never delays or replaces the
2466
+ // normal agent run.
2467
+ if (adapter.sendSystemNotice) {
2468
+ const senderEmail =
2469
+ typeof incoming.senderEmail === "string" &&
2470
+ incoming.senderEmail.trim()
2471
+ ? incoming.senderEmail.trim()
2472
+ : null;
2473
+ const noticeText = senderEmail
2474
+ ? `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.`
2475
+ : "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.";
2476
+ void withCredentialContext(credentialContext, () =>
2477
+ adapter.sendSystemNotice!(incoming!, noticeText, {
2478
+ dedupeKey: `anonymous-tier:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
2479
+ }),
2480
+ ).catch((noticeErr) => {
2481
+ console.warn(
2482
+ `[integrations] anonymous-tier notice failed:`,
2483
+ noticeErr instanceof Error ? noticeErr.message : noticeErr,
2484
+ );
2485
+ });
2486
+ }
2487
+ }
2416
2488
  } else if (options?.resolveOwner) {
2417
2489
  try {
2418
2490
  executionContext.ownerEmail = await withCredentialContext(
@@ -104,6 +104,13 @@ export interface IncomingMessage {
104
104
  targetAgent?: string;
105
105
  instruction: string;
106
106
  };
107
+ /**
108
+ * Trusted app-side note about the caller's identity/visibility tier, set
109
+ * after execution-context resolution (e.g. an anonymous org-scoped Slack
110
+ * member). Surfaced to the agent as integration context. Adapters must not
111
+ * copy this from user-controlled webhook payload fields.
112
+ */
113
+ identityNote?: string;
107
114
  /** Provider-native message/activity reference for contextual replies. */
108
115
  replyRef?: string;
109
116
  /** Message timestamp (epoch ms) */
@@ -311,6 +318,23 @@ export interface PlatformAdapter {
311
318
  opts?: { placeholderRef?: string },
312
319
  ): Promise<void>;
313
320
 
321
+ /**
322
+ * Send a short best-effort system notice to the conversation the incoming
323
+ * message arrived on (polite identity declines, one-time access guidance).
324
+ * Bypasses agent formatting. When `dedupeKey` is provided, the adapter may
325
+ * drop the notice if the same key was sent recently. Callers must treat
326
+ * failures as non-fatal.
327
+ */
328
+ sendSystemNotice?(
329
+ incoming: IncomingMessage,
330
+ text: string,
331
+ opts?: {
332
+ dedupeKey?: string;
333
+ /** Dedupe window for `dedupeKey`. Adapters pick a default when omitted. */
334
+ dedupeTtlMs?: number;
335
+ },
336
+ ): Promise<void>;
337
+
314
338
  /**
315
339
  * Optionally post a "working on it…" placeholder message immediately when a
316
340
  * webhook arrives, before the agent loop runs. Adapters that support
@@ -436,4 +460,10 @@ export interface IntegrationExecutionContext {
436
460
  principalType: "user" | "service";
437
461
  installationId?: string;
438
462
  scopeId?: string;
463
+ /**
464
+ * True when a hydrated full workspace member could not be matched to an
465
+ * organization member and runs with the anonymous org-scoped service
466
+ * principal (org-wide visibility only, nothing user-private).
467
+ */
468
+ anonymousMember?: boolean;
439
469
  }
@@ -745,6 +745,7 @@ async function processIncomingMessage(
745
745
  incoming.senderName ? `Sender name: ${incoming.senderName}` : null,
746
746
  incoming.senderEmail ? `Sender email: ${incoming.senderEmail}` : null,
747
747
  incoming.senderId ? `Sender ID: ${incoming.senderId}` : null,
748
+ incoming.identityNote ? `Caller identity: ${incoming.identityNote}` : null,
748
749
  incoming.sourceUrl ? `Source thread: ${incoming.sourceUrl}` : null,
749
750
  incoming.routingHint?.targetAgent
750
751
  ? `Required target agent: ${incoming.routingHint.targetAgent}`
@@ -858,8 +858,11 @@ function askAppTool(
858
858
  return {
859
859
  tool: tool(
860
860
  "Send a natural-language message to an app's AI agent and get its " +
861
- "response. Use for complex, multi-step tasks needing the agent's " +
862
- "reasoning and full app context. In a single-app project the 'app' " +
861
+ "response. Use this first for natural-language investigation, " +
862
+ "diagnosis, multi-step work, and changes; it runs with the app's " +
863
+ "full skills, instructions, tools, and context. Use direct action " +
864
+ "tools only for a known, bounded read or simple UI handoff. In a " +
865
+ "single-app project the 'app' " +
863
866
  "param is optional (defaults to this app). When 'app' names a " +
864
867
  "different workspace app it is routed there over A2A; the result's " +
865
868
  "'routedVia' field reports whether it ran cross-app or locally. " +
@@ -164,7 +164,10 @@ agent answers about browser recordings in the Analytics template.
164
164
  reintroducing a clamp) — as regression guards against reintroducing any
165
165
  viewport "recovery" or pointer-projection heuristic. Do not change their
166
166
  expectations merely to bless a new sanitizer or clamp; validate the affected
167
- replay in a browser first.
167
+ replay in a browser first. An interim clamp for the exact 3,189x885 pair was
168
+ also deleted once the view-time redaction root cause was proven; the earlier
169
+ 3,000-3,999px band was rejected because it also catches real 3440px-wide
170
+ displays. Neither the exact exception nor the band belongs in the player.
168
171
  - The event timeline soft-highlights the active marker, auto-scrolls it into
169
172
  view (pausing briefly after manual scroll), and supports search. It appears
170
173
  beside the player from ~880px content width upward.
@@ -124,15 +124,15 @@ details live in `.agents/skills/`.
124
124
  `userId`/email parameter (e.g. `userId: "user@example.com"`) or recording/
125
125
  issue id — never by matching emails read back from previously displayed
126
126
  output.
127
- - Analytics opts into the framework's authenticated-read connector policy, so
128
- every Analytics action explicitly marked GET + read-only +
129
- `publicAgent.requiresAuth` is directly callable by a signed-in external
130
- agent. This includes the incident reads above plus safe dashboard/analysis
131
- reads. Generic core `db-schema` / `db-query` remain in-app agent tools and
132
- are not automatically exposed because broad SQL/schema access is too
133
- powerful to infer from read-only metadata. Writes remain `ask_app`-only.
134
- Use the explicit catalog and `denyActions` policy for any unusually
135
- sensitive read instead of exposing an unannotated action.
127
+ - Analytics keeps its direct MCP surface explicitly curated, so external
128
+ agents should use `ask_app` for multi-step investigation and changes. The
129
+ six incident reads above are bounded, user/org-scoped fallback tools for
130
+ callers that already know which lookup they need. Generic core
131
+ `db-schema` / `db-query` remain in-app agent tools and are not exposed
132
+ directly because broad SQL/schema access is too powerful to infer from
133
+ read-only metadata. Writes remain `ask_app`-only. Use the explicit catalog
134
+ and `denyActions` policy for any unusually sensitive read instead of
135
+ exposing an unannotated action.
136
136
  - `/agents` is the Analytics home for admin surfaces. The default Monitoring
137
137
  view embeds the shared observability dashboard for traces, conversations,
138
138
  evals, experiments, and feedback. `/agents?view=dashboards` shows the
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Analytics uses the full in-app agent for multi-step incident investigations, with direct read tools available for focused lookups.