@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/docs/content/external-agents.mdx +15 -11
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +3 -1
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +3 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/handlers.ts +6 -5
- package/corpus/core/src/a2a/task-store.ts +63 -0
- package/corpus/core/src/client/session-replay.ts +67 -5
- package/corpus/core/src/integrations/adapters/slack.ts +99 -19
- package/corpus/core/src/integrations/identity.ts +103 -39
- package/corpus/core/src/integrations/plugin.ts +81 -9
- package/corpus/core/src/integrations/types.ts +30 -0
- package/corpus/core/src/integrations/webhook-handler.ts +1 -0
- package/corpus/core/src/mcp/builtin-tools.ts +5 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +4 -1
- package/corpus/templates/analytics/AGENTS.md +9 -9
- package/corpus/templates/analytics/changelog/2026-07-12-analytics-uses-the-full-in-app-agent-for-multi-step-incident.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +15 -9
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +23 -5
- package/corpus/templates/analytics/server/lib/session-replay.ts +26 -5
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -1
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +8 -5
- package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +8 -3
- package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +12 -2
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +6 -6
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/task-store.d.ts +12 -0
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +54 -0
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +53 -8
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- 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 +85 -11
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/identity.d.ts +19 -2
- package/dist/integrations/identity.d.ts.map +1 -1
- package/dist/integrations/identity.js +76 -34
- package/dist/integrations/identity.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +56 -6
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +25 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.js +1 -0
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +5 -2
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/external-agents.mdx +15 -11
- package/docs/content/locales/ar-SA/external-agents.mdx +3 -1
- package/docs/content/locales/de-DE/external-agents.mdx +3 -1
- package/docs/content/locales/es-ES/external-agents.mdx +3 -1
- package/docs/content/locales/fr-FR/external-agents.mdx +3 -1
- package/docs/content/locales/hi-IN/external-agents.mdx +3 -1
- package/docs/content/locales/ja-JP/external-agents.mdx +3 -1
- package/docs/content/locales/ko-KR/external-agents.mdx +3 -1
- package/docs/content/locales/pt-BR/external-agents.mdx +3 -1
- package/docs/content/locales/zh-CN/external-agents.mdx +3 -1
- package/docs/content/locales/zh-TW/external-agents.mdx +3 -1
- 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
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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)
|
|
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
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
69
|
-
*
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
101
|
-
incoming.actorTrust
|
|
127
|
+
incoming.actorTrust.memberType === "guest" ||
|
|
128
|
+
incoming.actorTrust.memberType === "external"
|
|
102
129
|
) {
|
|
103
|
-
throw new
|
|
104
|
-
"
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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:
|
|
128
|
-
orgId:
|
|
129
|
-
principalType: "
|
|
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 {
|
|
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
|
-
//
|
|
2382
|
-
// Slack
|
|
2383
|
-
//
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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
|
|
862
|
-
"
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|