@agent-native/core 0.98.6 → 0.98.8
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 +12 -0
- package/corpus/core/docs/content/external-agents.mdx +22 -12
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +8 -2
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +8 -2
- 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 +90 -6
- package/corpus/core/src/integrations/adapters/slack.ts +110 -19
- package/corpus/core/src/integrations/identity.ts +103 -39
- package/corpus/core/src/integrations/pending-tasks-retry-job.ts +2 -1
- package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
- package/corpus/core/src/integrations/plugin.ts +215 -8
- package/corpus/core/src/integrations/types.ts +37 -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 +71 -9
- 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 +94 -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/pending-tasks-retry-job.d.ts.map +1 -1
- package/dist/integrations/pending-tasks-retry-job.js +2 -1
- package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
- package/dist/integrations/pending-tasks-store.js +1 -1
- package/dist/integrations/pending-tasks-store.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +165 -9
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +32 -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/notifications/routes.d.ts +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 +22 -12
- package/docs/content/locales/ar-SA/external-agents.mdx +8 -2
- package/docs/content/locales/de-DE/external-agents.mdx +8 -2
- package/docs/content/locales/es-ES/external-agents.mdx +8 -2
- package/docs/content/locales/fr-FR/external-agents.mdx +8 -2
- package/docs/content/locales/hi-IN/external-agents.mdx +8 -2
- package/docs/content/locales/ja-JP/external-agents.mdx +8 -2
- package/docs/content/locales/ko-KR/external-agents.mdx +8 -2
- package/docs/content/locales/pt-BR/external-agents.mdx +8 -2
- package/docs/content/locales/zh-CN/external-agents.mdx +8 -2
- package/docs/content/locales/zh-TW/external-agents.mdx +8 -2
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -77,6 +80,7 @@ import {
|
|
|
77
80
|
claimPendingTask,
|
|
78
81
|
getNextPendingTaskIdForThread,
|
|
79
82
|
insertPendingTask,
|
|
83
|
+
isDuplicateEventError,
|
|
80
84
|
MAX_PENDING_TASK_ATTEMPTS,
|
|
81
85
|
markTaskCompleted,
|
|
82
86
|
markTaskFailed,
|
|
@@ -137,6 +141,7 @@ import type {
|
|
|
137
141
|
IntegrationsPluginOptions,
|
|
138
142
|
IntegrationStatus,
|
|
139
143
|
IntegrationExecutionContext,
|
|
144
|
+
IncomingMessage,
|
|
140
145
|
} from "./types.js";
|
|
141
146
|
import {
|
|
142
147
|
listIntegrationUsageBudgets,
|
|
@@ -252,6 +257,28 @@ type IntegrationCredentialContext = {
|
|
|
252
257
|
|
|
253
258
|
const REMOTE_DEVICE_ONLINE_MS = 90_000;
|
|
254
259
|
|
|
260
|
+
// One decline reply per sender + decline reason per window: during a Slack
|
|
261
|
+
// API outage every message would otherwise get another identical "try again"
|
|
262
|
+
// reply. Short enough that a persistent condition still reminds the sender.
|
|
263
|
+
const DECLINE_NOTICE_DEDUPE_TTL_MS = 5 * 60 * 1_000;
|
|
264
|
+
const SYSTEM_NOTICE_DEDUPE_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
265
|
+
|
|
266
|
+
type IntegrationSystemNoticeTaskPayload = {
|
|
267
|
+
kind: "system-notice";
|
|
268
|
+
incoming: IncomingMessage;
|
|
269
|
+
text: string;
|
|
270
|
+
dedupeKey?: string;
|
|
271
|
+
dedupeTtlMs?: number;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
function systemNoticeEventKey(
|
|
275
|
+
dedupeKey: string,
|
|
276
|
+
ttlMs: number,
|
|
277
|
+
now = Date.now(),
|
|
278
|
+
): string {
|
|
279
|
+
return `system-notice:${dedupeKey}:${Math.floor(now / ttlMs)}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
255
282
|
export async function enqueueRemoteCommand(
|
|
256
283
|
envelope: RemoteCodeCommandEnvelope,
|
|
257
284
|
): Promise<Record<string, unknown>> {
|
|
@@ -629,6 +656,66 @@ export function createIntegrationsPlugin(
|
|
|
629
656
|
const h3 = getH3App(nitroApp);
|
|
630
657
|
const P = `${FRAMEWORK_ROUTE_PREFIX}/integrations`;
|
|
631
658
|
|
|
659
|
+
async function enqueueSystemNotice(
|
|
660
|
+
event: any,
|
|
661
|
+
incoming: IncomingMessage,
|
|
662
|
+
text: string,
|
|
663
|
+
opts?: { dedupeKey?: string; dedupeTtlMs?: number },
|
|
664
|
+
): Promise<void> {
|
|
665
|
+
if (!text.trim()) return;
|
|
666
|
+
const taskId = `notice-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
667
|
+
const dedupeTtlMs = Math.max(
|
|
668
|
+
1,
|
|
669
|
+
opts?.dedupeTtlMs ?? SYSTEM_NOTICE_DEDUPE_TTL_MS,
|
|
670
|
+
);
|
|
671
|
+
const noticeThreadId = `system-notice:${taskId}`;
|
|
672
|
+
const payload: IntegrationSystemNoticeTaskPayload = {
|
|
673
|
+
kind: "system-notice",
|
|
674
|
+
incoming,
|
|
675
|
+
text,
|
|
676
|
+
...(opts?.dedupeKey ? { dedupeKey: opts.dedupeKey } : {}),
|
|
677
|
+
...(opts?.dedupeTtlMs ? { dedupeTtlMs: opts.dedupeTtlMs } : {}),
|
|
678
|
+
};
|
|
679
|
+
try {
|
|
680
|
+
await insertPendingTask({
|
|
681
|
+
id: taskId,
|
|
682
|
+
platform: incoming.platform,
|
|
683
|
+
// System notices are auxiliary delivery work, not the user's agent
|
|
684
|
+
// run. Give each notice its own queue lane so a retrying notice cannot
|
|
685
|
+
// block the real message task for this Slack/Telegram thread.
|
|
686
|
+
externalThreadId: noticeThreadId,
|
|
687
|
+
payload: JSON.stringify(payload),
|
|
688
|
+
ownerEmail: `integration@${incoming.platform}`,
|
|
689
|
+
externalEventKey: opts?.dedupeKey
|
|
690
|
+
? systemNoticeEventKey(opts.dedupeKey, dedupeTtlMs)
|
|
691
|
+
: undefined,
|
|
692
|
+
});
|
|
693
|
+
} catch (err) {
|
|
694
|
+
if (isDuplicateEventError(err)) return;
|
|
695
|
+
throw err;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// The SQL row is the durable source of truth. This best-effort self-call
|
|
699
|
+
// only reduces latency; the recurring pending-task sweep retries a row
|
|
700
|
+
// if the serverless host freezes this webhook execution immediately.
|
|
701
|
+
let token: string | undefined;
|
|
702
|
+
try {
|
|
703
|
+
token = signInternalToken(taskId);
|
|
704
|
+
} catch (err) {
|
|
705
|
+
if (process.env.NODE_ENV === "production") throw err;
|
|
706
|
+
}
|
|
707
|
+
void fetch(`${getBaseUrl(event)}${P}/process-task`, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: {
|
|
710
|
+
"Content-Type": "application/json",
|
|
711
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
712
|
+
},
|
|
713
|
+
body: JSON.stringify({ taskId }),
|
|
714
|
+
}).catch((err) => {
|
|
715
|
+
console.warn("[integrations] System notice dispatch failed:", err);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
632
719
|
async function requireSession(event: any): Promise<boolean> {
|
|
633
720
|
const session = await getSession(event).catch(() => null);
|
|
634
721
|
if (session?.email) return true;
|
|
@@ -1542,6 +1629,34 @@ export function createIntegrationsPlugin(
|
|
|
1542
1629
|
isIntegrationCaller: true,
|
|
1543
1630
|
},
|
|
1544
1631
|
async () => {
|
|
1632
|
+
const taskPayload = JSON.parse(task.payload) as
|
|
1633
|
+
| IntegrationSystemNoticeTaskPayload
|
|
1634
|
+
| { kind?: undefined };
|
|
1635
|
+
if (taskPayload.kind === "system-notice") {
|
|
1636
|
+
if (!adapter.sendSystemNotice) {
|
|
1637
|
+
throw new Error(
|
|
1638
|
+
`Platform ${task.platform} cannot deliver system notices`,
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
const config = await getIntegrationConfig(task.platform);
|
|
1642
|
+
const credentialContext =
|
|
1643
|
+
await credentialContextForIntegrationConfig(config);
|
|
1644
|
+
await withCredentialContext(credentialContext, () =>
|
|
1645
|
+
adapter.sendSystemNotice!(
|
|
1646
|
+
taskPayload.incoming,
|
|
1647
|
+
taskPayload.text,
|
|
1648
|
+
{
|
|
1649
|
+
...(taskPayload.dedupeKey
|
|
1650
|
+
? { dedupeKey: taskPayload.dedupeKey }
|
|
1651
|
+
: {}),
|
|
1652
|
+
...(taskPayload.dedupeTtlMs
|
|
1653
|
+
? { dedupeTtlMs: taskPayload.dedupeTtlMs }
|
|
1654
|
+
: {}),
|
|
1655
|
+
},
|
|
1656
|
+
),
|
|
1657
|
+
);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1545
1660
|
const resources = await loadResourcesForPrompt(
|
|
1546
1661
|
task.ownerEmail,
|
|
1547
1662
|
true,
|
|
@@ -2370,7 +2485,8 @@ export function createIntegrationsPlugin(
|
|
|
2370
2485
|
null;
|
|
2371
2486
|
if (
|
|
2372
2487
|
incoming.platform === "slack" &&
|
|
2373
|
-
incoming.conversationType === "dm"
|
|
2488
|
+
incoming.conversationType === "dm" &&
|
|
2489
|
+
!options?.resolveExecutionContext
|
|
2374
2490
|
) {
|
|
2375
2491
|
try {
|
|
2376
2492
|
defaultExecutionContext = await withCredentialContext(
|
|
@@ -2378,17 +2494,48 @@ export function createIntegrationsPlugin(
|
|
|
2378
2494
|
() => resolveDefaultIntegrationExecutionContext(incoming!),
|
|
2379
2495
|
);
|
|
2380
2496
|
} catch (err) {
|
|
2381
|
-
//
|
|
2382
|
-
//
|
|
2383
|
-
//
|
|
2384
|
-
|
|
2497
|
+
// The legacy owner-only resolver predates org-bound identities
|
|
2498
|
+
// and must not turn a rejected Slack DM into an authenticated
|
|
2499
|
+
// owner run. Custom resolveExecutionContext is checked above and
|
|
2500
|
+
// skips this default ladder entirely so apps can fully own auth
|
|
2501
|
+
// without framework membership checks or identity side effects.
|
|
2502
|
+
const declined =
|
|
2503
|
+
err instanceof IntegrationIdentityDeclinedError ? err : null;
|
|
2504
|
+
if (declined) {
|
|
2505
|
+
console.warn(
|
|
2506
|
+
`[integrations] default Slack DM identity declined message:`,
|
|
2507
|
+
declined.message,
|
|
2508
|
+
);
|
|
2509
|
+
if (adapter.sendSystemNotice) {
|
|
2510
|
+
try {
|
|
2511
|
+
await enqueueSystemNotice(
|
|
2512
|
+
event,
|
|
2513
|
+
incoming!,
|
|
2514
|
+
declined.userFacingMessage,
|
|
2515
|
+
{
|
|
2516
|
+
dedupeKey: `decline:${incoming!.tenantId ?? "unknown"}:${incoming!.senderId ?? "unknown"}:${declined.reason}`,
|
|
2517
|
+
dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
|
|
2518
|
+
},
|
|
2519
|
+
);
|
|
2520
|
+
} catch (noticeErr) {
|
|
2521
|
+
console.warn(
|
|
2522
|
+
`[integrations] could not persist decline notice:`,
|
|
2523
|
+
noticeErr instanceof Error
|
|
2524
|
+
? noticeErr.message
|
|
2525
|
+
: noticeErr,
|
|
2526
|
+
);
|
|
2527
|
+
setResponseStatus(event, 500);
|
|
2528
|
+
return { error: "notice enqueue failed" };
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
} else {
|
|
2385
2532
|
console.error(
|
|
2386
2533
|
`[integrations] default Slack DM identity denied message:`,
|
|
2387
2534
|
err,
|
|
2388
2535
|
);
|
|
2389
|
-
setResponseStatus(event, 200);
|
|
2390
|
-
return "ok";
|
|
2391
2536
|
}
|
|
2537
|
+
setResponseStatus(event, 200);
|
|
2538
|
+
return "ok";
|
|
2392
2539
|
}
|
|
2393
2540
|
}
|
|
2394
2541
|
let executionContext: IntegrationExecutionContext = {
|
|
@@ -2413,6 +2560,66 @@ export function createIntegrationsPlugin(
|
|
|
2413
2560
|
}
|
|
2414
2561
|
} else if (defaultExecutionContext) {
|
|
2415
2562
|
executionContext = defaultExecutionContext;
|
|
2563
|
+
if (defaultExecutionContext.anonymousMember) {
|
|
2564
|
+
if (!options?.allowAnonymousOrgScopedSlackDm) {
|
|
2565
|
+
const senderEmail =
|
|
2566
|
+
typeof incoming.senderEmail === "string" &&
|
|
2567
|
+
incoming.senderEmail.trim()
|
|
2568
|
+
? incoming.senderEmail.trim()
|
|
2569
|
+
: null;
|
|
2570
|
+
const noticeText = senderEmail
|
|
2571
|
+
? `I couldn't match your Slack account to an organization member, so I can't run this request. Ask an organization admin to add ${senderEmail}, then try again.`
|
|
2572
|
+
: "I couldn't verify your Slack account email, so I can't run this request. Ask an organization admin to reconnect Slack with the users:read.email scope, then try again.";
|
|
2573
|
+
if (adapter.sendSystemNotice) {
|
|
2574
|
+
try {
|
|
2575
|
+
await enqueueSystemNotice(event, incoming, noticeText, {
|
|
2576
|
+
dedupeKey: `anonymous-tier-disabled:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
|
|
2577
|
+
});
|
|
2578
|
+
} catch (noticeErr) {
|
|
2579
|
+
console.warn(
|
|
2580
|
+
`[integrations] could not persist unlinked-member notice:`,
|
|
2581
|
+
noticeErr instanceof Error
|
|
2582
|
+
? noticeErr.message
|
|
2583
|
+
: noticeErr,
|
|
2584
|
+
);
|
|
2585
|
+
setResponseStatus(event, 500);
|
|
2586
|
+
return { error: "notice enqueue failed" };
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
setResponseStatus(event, 200);
|
|
2590
|
+
return "ok";
|
|
2591
|
+
}
|
|
2592
|
+
// The anonymous tier must never be silent. (1) The agent run
|
|
2593
|
+
// can tell: the note rides the serialized `incoming` into the
|
|
2594
|
+
// queued task and surfaces via <integration-context>.
|
|
2595
|
+
incoming.identityNote =
|
|
2596
|
+
"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).";
|
|
2597
|
+
// (2) The sender gets a one-time heads-up through the same
|
|
2598
|
+
// durable SQL queue as agent work. The self-dispatch is only a
|
|
2599
|
+
// latency optimization; the retry sweep guarantees delivery.
|
|
2600
|
+
if (adapter.sendSystemNotice) {
|
|
2601
|
+
const senderEmail =
|
|
2602
|
+
typeof incoming.senderEmail === "string" &&
|
|
2603
|
+
incoming.senderEmail.trim()
|
|
2604
|
+
? incoming.senderEmail.trim()
|
|
2605
|
+
: null;
|
|
2606
|
+
const noticeText = senderEmail
|
|
2607
|
+
? `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.`
|
|
2608
|
+
: "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.";
|
|
2609
|
+
try {
|
|
2610
|
+
await enqueueSystemNotice(event, incoming, noticeText, {
|
|
2611
|
+
dedupeKey: `anonymous-tier:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
|
|
2612
|
+
});
|
|
2613
|
+
} catch (noticeErr) {
|
|
2614
|
+
console.warn(
|
|
2615
|
+
`[integrations] could not persist anonymous-tier notice:`,
|
|
2616
|
+
noticeErr instanceof Error ? noticeErr.message : noticeErr,
|
|
2617
|
+
);
|
|
2618
|
+
setResponseStatus(event, 500);
|
|
2619
|
+
return { error: "notice enqueue failed" };
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2416
2623
|
} else if (options?.resolveOwner) {
|
|
2417
2624
|
try {
|
|
2418
2625
|
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
|
|
@@ -414,6 +438,13 @@ export interface IntegrationsPluginOptions {
|
|
|
414
438
|
resolveExecutionContext?: (
|
|
415
439
|
incoming: IncomingMessage,
|
|
416
440
|
) => IntegrationExecutionContext | Promise<IntegrationExecutionContext>;
|
|
441
|
+
/**
|
|
442
|
+
* Explicitly allow an unlinked, verified Slack workspace member to run a DM
|
|
443
|
+
* with the installation organization's shared/service visibility. Disabled
|
|
444
|
+
* by default: DM identity resolution fails closed unless an app deliberately
|
|
445
|
+
* accepts this wider access tier.
|
|
446
|
+
*/
|
|
447
|
+
allowAnonymousOrgScopedSlackDm?: boolean;
|
|
417
448
|
/**
|
|
418
449
|
* Optional preprocessor for inbound platform messages. Can intercept special
|
|
419
450
|
* commands (such as `/link`) before the agent loop runs.
|
|
@@ -436,4 +467,10 @@ export interface IntegrationExecutionContext {
|
|
|
436
467
|
principalType: "user" | "service";
|
|
437
468
|
installationId?: string;
|
|
438
469
|
scopeId?: string;
|
|
470
|
+
/**
|
|
471
|
+
* True when a hydrated full workspace member could not be matched to an
|
|
472
|
+
* organization member and runs with the anonymous org-scoped service
|
|
473
|
+
* principal (org-wide visibility only, nothing user-private).
|
|
474
|
+
*/
|
|
475
|
+
anonymousMember?: boolean;
|
|
439
476
|
}
|
|
@@ -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
|
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import { runApiHandlerWithContext } from "../lib/credentials";
|
|
14
14
|
import {
|
|
15
|
+
resolveSessionReplayAgentAccess,
|
|
15
16
|
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
16
|
-
verifySessionReplayAgentAccess,
|
|
17
17
|
} from "../lib/session-replay-agent-context.js";
|
|
18
18
|
import {
|
|
19
19
|
getSessionReplayManifest,
|
|
@@ -236,12 +236,13 @@ function replayChunkSeqsFromQuery(query: Record<string, unknown>): number[] {
|
|
|
236
236
|
function verifyAgentReplayAccessToken(
|
|
237
237
|
event: any,
|
|
238
238
|
recordingId: string,
|
|
239
|
-
): string | null {
|
|
239
|
+
): { token: string; viewerEmail: string } | null | false {
|
|
240
240
|
const token = readAgentReplayAccessToken(event);
|
|
241
241
|
if (!token) return null;
|
|
242
|
-
|
|
242
|
+
const access = resolveSessionReplayAgentAccess(recordingId, token);
|
|
243
|
+
if (access) return { token, viewerEmail: access.viewerEmail };
|
|
243
244
|
setResponseStatus(event, 401);
|
|
244
|
-
return
|
|
245
|
+
return false;
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
function listFiltersFromQuery(
|
|
@@ -371,13 +372,16 @@ export const handleSessionReplayManifest = defineEventHandler(async (event) => {
|
|
|
371
372
|
}
|
|
372
373
|
|
|
373
374
|
const agentAccessToken = verifyAgentReplayAccessToken(event, recordingId);
|
|
374
|
-
if (agentAccessToken ===
|
|
375
|
+
if (agentAccessToken === false) {
|
|
375
376
|
return { error: "Invalid or expired agent access" };
|
|
376
377
|
}
|
|
377
378
|
if (agentAccessToken) {
|
|
378
379
|
try {
|
|
379
380
|
applyAgentReplayReadHeaders(event);
|
|
380
|
-
const manifest = await getSessionReplayTokenizedManifest(
|
|
381
|
+
const manifest = await getSessionReplayTokenizedManifest(
|
|
382
|
+
recordingId,
|
|
383
|
+
agentAccessToken.viewerEmail,
|
|
384
|
+
);
|
|
381
385
|
return {
|
|
382
386
|
...manifest,
|
|
383
387
|
chunks: manifest.chunks.map((chunk) => ({
|
|
@@ -385,7 +389,7 @@ export const handleSessionReplayManifest = defineEventHandler(async (event) => {
|
|
|
385
389
|
bytesPath: appendQueryParam(
|
|
386
390
|
chunk.bytesPath,
|
|
387
391
|
SESSION_REPLAY_AGENT_ACCESS_PARAM,
|
|
388
|
-
agentAccessToken,
|
|
392
|
+
agentAccessToken.token,
|
|
389
393
|
),
|
|
390
394
|
})),
|
|
391
395
|
};
|
|
@@ -419,7 +423,7 @@ export const handleSessionReplayChunkBytes = defineEventHandler(
|
|
|
419
423
|
}
|
|
420
424
|
|
|
421
425
|
const agentAccessToken = verifyAgentReplayAccessToken(event, recordingId);
|
|
422
|
-
if (agentAccessToken ===
|
|
426
|
+
if (agentAccessToken === false) {
|
|
423
427
|
return { error: "Invalid or expired agent access" };
|
|
424
428
|
}
|
|
425
429
|
if (agentAccessToken) {
|
|
@@ -427,6 +431,7 @@ export const handleSessionReplayChunkBytes = defineEventHandler(
|
|
|
427
431
|
const result = await readSessionReplayTokenizedChunkBytes(
|
|
428
432
|
recordingId,
|
|
429
433
|
seq,
|
|
434
|
+
agentAccessToken.viewerEmail,
|
|
430
435
|
);
|
|
431
436
|
applyAgentReplayReadHeaders(event);
|
|
432
437
|
setResponseHeader(event, "Content-Type", "application/json");
|
|
@@ -472,7 +477,7 @@ export const handleSessionReplayChunkBatch = defineEventHandler(
|
|
|
472
477
|
const seqs = replayChunkSeqsFromQuery(getQuery(event));
|
|
473
478
|
|
|
474
479
|
const agentAccessToken = verifyAgentReplayAccessToken(event, recordingId);
|
|
475
|
-
if (agentAccessToken ===
|
|
480
|
+
if (agentAccessToken === false) {
|
|
476
481
|
return { error: "Invalid or expired agent access" };
|
|
477
482
|
}
|
|
478
483
|
if (agentAccessToken) {
|
|
@@ -480,6 +485,7 @@ export const handleSessionReplayChunkBatch = defineEventHandler(
|
|
|
480
485
|
const result = await readSessionReplayTokenizedChunkBatch(
|
|
481
486
|
recordingId,
|
|
482
487
|
seqs,
|
|
488
|
+
agentAccessToken.viewerEmail,
|
|
483
489
|
);
|
|
484
490
|
applyAgentReplayReadHeaders(event);
|
|
485
491
|
setResponseHeader(event, "Content-Type", "application/json");
|
|
@@ -651,10 +651,22 @@ export function verifySessionReplayAgentAccess(
|
|
|
651
651
|
recordingId: string,
|
|
652
652
|
token: string,
|
|
653
653
|
): boolean {
|
|
654
|
-
return
|
|
654
|
+
return resolveSessionReplayAgentAccess(recordingId, token) !== null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function resolveSessionReplayAgentAccess(
|
|
658
|
+
recordingId: string,
|
|
659
|
+
token: string,
|
|
660
|
+
): { viewerEmail: string } | null {
|
|
661
|
+
const result = verifyScopedAgentAccessToken(token, {
|
|
655
662
|
resourceKind: SESSION_REPLAY_AGENT_ACCESS_TOKEN_PREFIX,
|
|
656
663
|
resourceId: recordingId,
|
|
657
|
-
})
|
|
664
|
+
});
|
|
665
|
+
// Replay grants are always minted by an authenticated viewer. Fail closed
|
|
666
|
+
// for grants without that signed identity: demo-mode policy is viewer-scoped,
|
|
667
|
+
// so guessing from the ambient request could expose identities to agents.
|
|
668
|
+
if (!result.ok || !result.viewerEmail) return null;
|
|
669
|
+
return { viewerEmail: result.viewerEmail };
|
|
658
670
|
}
|
|
659
671
|
|
|
660
672
|
export async function createSessionReplayAgentLink({
|
|
@@ -707,14 +719,18 @@ export async function buildSessionReplayAgentContext({
|
|
|
707
719
|
origin?: string;
|
|
708
720
|
includeTimeline?: boolean;
|
|
709
721
|
}) {
|
|
710
|
-
|
|
722
|
+
const access = resolveSessionReplayAgentAccess(recordingId, token);
|
|
723
|
+
if (!access) {
|
|
711
724
|
const error = Object.assign(new Error("Invalid or expired agent access"), {
|
|
712
725
|
statusCode: 401,
|
|
713
726
|
});
|
|
714
727
|
throw error;
|
|
715
728
|
}
|
|
716
729
|
|
|
717
|
-
const recording = await getSessionReplayTokenizedSummary(
|
|
730
|
+
const recording = await getSessionReplayTokenizedSummary(
|
|
731
|
+
recordingId,
|
|
732
|
+
access.viewerEmail,
|
|
733
|
+
);
|
|
718
734
|
const resolvedOrigin = appOrigin(origin);
|
|
719
735
|
const basePath = appBasePath();
|
|
720
736
|
const contextUrl = buildAgentAccessApiUrl({
|
|
@@ -747,7 +763,9 @@ export async function buildSessionReplayAgentContext({
|
|
|
747
763
|
});
|
|
748
764
|
|
|
749
765
|
const eventsResponse = includeTimeline
|
|
750
|
-
? await getSessionReplayTokenizedEvents(recording.id, {
|
|
766
|
+
? await getSessionReplayTokenizedEvents(recording.id, access.viewerEmail, {
|
|
767
|
+
limit: 10000,
|
|
768
|
+
})
|
|
751
769
|
: null;
|
|
752
770
|
const events =
|
|
753
771
|
eventsResponse?.chunks.flatMap((chunk) =>
|
|
@@ -1786,8 +1786,13 @@ export async function getSessionReplaySummary(
|
|
|
1786
1786
|
|
|
1787
1787
|
export async function getSessionReplayTokenizedSummary(
|
|
1788
1788
|
recordingId: string,
|
|
1789
|
+
viewerEmail: string,
|
|
1789
1790
|
): Promise<SessionRecordingSummary> {
|
|
1790
|
-
|
|
1791
|
+
// Tokenized reads often run without an authenticated request session. Use
|
|
1792
|
+
// the viewer identity embedded in the signed, recording-scoped grant so a
|
|
1793
|
+
// link minted while demo mode is enabled cannot reveal real identities when
|
|
1794
|
+
// it is opened in a signed-out browser or by an external agent.
|
|
1795
|
+
const demoMode = await isSessionDemoModeEnabled(viewerEmail);
|
|
1791
1796
|
const db = getDb() as any;
|
|
1792
1797
|
// guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
|
|
1793
1798
|
const [row] = await db
|
|
@@ -1847,6 +1852,7 @@ export async function getSessionReplayManifest(
|
|
|
1847
1852
|
|
|
1848
1853
|
export async function getSessionReplayTokenizedManifest(
|
|
1849
1854
|
recordingId: string,
|
|
1855
|
+
viewerEmail: string,
|
|
1850
1856
|
): Promise<{
|
|
1851
1857
|
recording: AgentSessionRecordingSummary;
|
|
1852
1858
|
chunks: Array<{
|
|
@@ -1859,7 +1865,10 @@ export async function getSessionReplayTokenizedManifest(
|
|
|
1859
1865
|
bytesPath: string;
|
|
1860
1866
|
}>;
|
|
1861
1867
|
}> {
|
|
1862
|
-
const recording = await getSessionReplayTokenizedSummary(
|
|
1868
|
+
const recording = await getSessionReplayTokenizedSummary(
|
|
1869
|
+
recordingId,
|
|
1870
|
+
viewerEmail,
|
|
1871
|
+
);
|
|
1863
1872
|
const manifest = await getSessionReplayManifestForRecording(recording);
|
|
1864
1873
|
return {
|
|
1865
1874
|
...manifest,
|
|
@@ -1923,6 +1932,7 @@ export async function readSessionReplayChunkBytes(
|
|
|
1923
1932
|
export async function readSessionReplayTokenizedChunkBytes(
|
|
1924
1933
|
recordingId: string,
|
|
1925
1934
|
seq: number,
|
|
1935
|
+
viewerEmail: string,
|
|
1926
1936
|
): Promise<{
|
|
1927
1937
|
recording: AgentSessionRecordingSummary;
|
|
1928
1938
|
seq: number;
|
|
@@ -1930,7 +1940,10 @@ export async function readSessionReplayTokenizedChunkBytes(
|
|
|
1930
1940
|
/** Decompressed replay-chunk JSON text (a serialized rrweb events array). */
|
|
1931
1941
|
json: string;
|
|
1932
1942
|
}> {
|
|
1933
|
-
const recording = await getSessionReplayTokenizedSummary(
|
|
1943
|
+
const recording = await getSessionReplayTokenizedSummary(
|
|
1944
|
+
recordingId,
|
|
1945
|
+
viewerEmail,
|
|
1946
|
+
);
|
|
1934
1947
|
const chunk = await readSessionReplayChunkBytesForRecording(recording, seq);
|
|
1935
1948
|
return {
|
|
1936
1949
|
...chunk,
|
|
@@ -2175,8 +2188,12 @@ export async function readSessionReplayChunkBatch(
|
|
|
2175
2188
|
export async function readSessionReplayTokenizedChunkBatch(
|
|
2176
2189
|
recordingId: string,
|
|
2177
2190
|
seqs: number[],
|
|
2191
|
+
viewerEmail: string,
|
|
2178
2192
|
): Promise<SessionReplayChunkBatchResult> {
|
|
2179
|
-
const recording = await getSessionReplayTokenizedSummary(
|
|
2193
|
+
const recording = await getSessionReplayTokenizedSummary(
|
|
2194
|
+
recordingId,
|
|
2195
|
+
viewerEmail,
|
|
2196
|
+
);
|
|
2180
2197
|
return readSessionReplayChunkBatchForRecording(recording, seqs);
|
|
2181
2198
|
}
|
|
2182
2199
|
|
|
@@ -2204,6 +2221,7 @@ export async function getSessionReplayEvents(
|
|
|
2204
2221
|
|
|
2205
2222
|
export async function getSessionReplayTokenizedEvents(
|
|
2206
2223
|
recordingId: string,
|
|
2224
|
+
viewerEmail: string,
|
|
2207
2225
|
options: SessionReplayEventReadOptions = {},
|
|
2208
2226
|
): Promise<{
|
|
2209
2227
|
recording: AgentSessionRecordingSummary;
|
|
@@ -2219,7 +2237,10 @@ export async function getSessionReplayTokenizedEvents(
|
|
|
2219
2237
|
truncated: boolean;
|
|
2220
2238
|
unavailableChunks: number;
|
|
2221
2239
|
}> {
|
|
2222
|
-
const recording = await getSessionReplayTokenizedSummary(
|
|
2240
|
+
const recording = await getSessionReplayTokenizedSummary(
|
|
2241
|
+
recordingId,
|
|
2242
|
+
viewerEmail,
|
|
2243
|
+
);
|
|
2223
2244
|
const result = await getSessionReplayEventsForRecording(recording, options);
|
|
2224
2245
|
return {
|
|
2225
2246
|
...result,
|
|
@@ -259,7 +259,10 @@ export default createAgentChatPlugin({
|
|
|
259
259
|
codeExecution: { production: "sandboxed" },
|
|
260
260
|
connectorCatalog: [...ANALYTICS_CONNECTOR_CATALOG],
|
|
261
261
|
externalAgents: {
|
|
262
|
-
|
|
262
|
+
// Keep the direct MCP surface deliberately curated. External agents
|
|
263
|
+
// should use ask_app for multi-step investigation; these six actions are
|
|
264
|
+
// bounded fallback reads for callers that already know what they need.
|
|
265
|
+
authenticatedReads: "off",
|
|
263
266
|
writes: "ask_app_only",
|
|
264
267
|
},
|
|
265
268
|
resolveOrgId: async (event) => {
|