@agent-native/core 0.98.5 → 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 (135) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +18 -0
  3. package/corpus/core/docs/content/external-agents.mdx +66 -7
  4. package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +40 -7
  5. package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +41 -7
  6. package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +41 -7
  7. package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +41 -7
  8. package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +40 -7
  9. package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +40 -7
  10. package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +40 -7
  11. package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +41 -7
  12. package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +39 -7
  13. package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +39 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/handlers.ts +101 -26
  16. package/corpus/core/src/a2a/task-store.ts +128 -3
  17. package/corpus/core/src/client/session-replay.ts +67 -5
  18. package/corpus/core/src/integrations/adapters/slack.ts +207 -17
  19. package/corpus/core/src/integrations/identity-links-store.ts +210 -0
  20. package/corpus/core/src/integrations/identity.ts +196 -0
  21. package/corpus/core/src/integrations/index.ts +6 -0
  22. package/corpus/core/src/integrations/plugin.ts +130 -1
  23. package/corpus/core/src/integrations/types.ts +37 -0
  24. package/corpus/core/src/integrations/webhook-handler.ts +1 -0
  25. package/corpus/core/src/mcp/build-server.ts +127 -22
  26. package/corpus/core/src/mcp/builtin-tools.ts +5 -2
  27. package/corpus/core/src/mcp/external-agent-policy.ts +18 -0
  28. package/corpus/core/src/mcp/index.ts +1 -0
  29. package/corpus/core/src/server/agent-chat/plugin-options.ts +12 -1
  30. package/corpus/core/src/server/agent-chat/script-entries.ts +20 -3
  31. package/corpus/core/src/server/agent-chat-plugin.ts +3 -0
  32. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  33. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +4 -1
  34. package/corpus/templates/analytics/AGENTS.md +20 -5
  35. package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +0 -1
  36. package/corpus/templates/analytics/actions/get-session-replay-events.ts +0 -1
  37. package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +40 -0
  38. package/corpus/templates/analytics/actions/list-error-issues.ts +12 -0
  39. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -1
  40. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +48 -17
  41. package/corpus/templates/analytics/changelog/2026-07-12-analytics-uses-the-full-in-app-agent-for-multi-step-incident.md +6 -0
  42. package/corpus/templates/analytics/changelog/2026-07-12-connected-external-agents-can-now-look-up-sessions-error-iss.md +6 -0
  43. package/corpus/templates/analytics/changelog/2026-07-12-session-identities-stay-visible-when-demo-mode-is-off.md +6 -0
  44. package/corpus/templates/analytics/server/handlers/session-replay.ts +15 -9
  45. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +2 -2
  46. package/corpus/templates/analytics/server/lib/error-capture.ts +47 -5
  47. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +72 -22
  48. package/corpus/templates/analytics/server/lib/session-replay.ts +41 -11
  49. package/corpus/templates/analytics/server/plugins/agent-chat.ts +7 -0
  50. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  51. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +8 -5
  52. package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +8 -3
  53. package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +12 -2
  54. package/corpus/templates/forms/app/global.css +54 -8
  55. package/corpus/templates/forms/app/pages/AskPage.tsx +35 -13
  56. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -17
  57. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +8 -5
  58. package/corpus/templates/forms/changelog/2026-07-12-ask-forms-suggestions-now-sit-beneath-the-composer-for-a-tig.md +6 -0
  59. package/corpus/templates/forms/changelog/2026-07-12-builder-response-tables-now-fill-the-view-without-a-redundan.md +6 -0
  60. package/corpus/templates/forms/changelog/2026-07-12-response-tables-now-fill-the-available-pane-with-a-flexible-.md +6 -0
  61. package/corpus/templates/mail/AGENTS.md +3 -1
  62. package/corpus/templates/mail/server/lib/mail-integrations.ts +3 -0
  63. package/dist/a2a/handlers.d.ts.map +1 -1
  64. package/dist/a2a/handlers.js +89 -20
  65. package/dist/a2a/handlers.js.map +1 -1
  66. package/dist/a2a/task-store.d.ts +32 -1
  67. package/dist/a2a/task-store.d.ts.map +1 -1
  68. package/dist/a2a/task-store.js +111 -4
  69. package/dist/a2a/task-store.js.map +1 -1
  70. package/dist/client/session-replay.d.ts.map +1 -1
  71. package/dist/client/session-replay.js +53 -8
  72. package/dist/client/session-replay.js.map +1 -1
  73. package/dist/collab/routes.d.ts +1 -1
  74. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  75. package/dist/integrations/adapters/slack.js +176 -17
  76. package/dist/integrations/adapters/slack.js.map +1 -1
  77. package/dist/integrations/identity-links-store.d.ts +23 -0
  78. package/dist/integrations/identity-links-store.d.ts.map +1 -0
  79. package/dist/integrations/identity-links-store.js +158 -0
  80. package/dist/integrations/identity-links-store.js.map +1 -0
  81. package/dist/integrations/identity.d.ts +28 -0
  82. package/dist/integrations/identity.d.ts.map +1 -0
  83. package/dist/integrations/identity.js +142 -0
  84. package/dist/integrations/identity.js.map +1 -0
  85. package/dist/integrations/index.d.ts +2 -0
  86. package/dist/integrations/index.d.ts.map +1 -1
  87. package/dist/integrations/index.js +2 -0
  88. package/dist/integrations/index.js.map +1 -1
  89. package/dist/integrations/plugin.d.ts.map +1 -1
  90. package/dist/integrations/plugin.js +93 -1
  91. package/dist/integrations/plugin.js.map +1 -1
  92. package/dist/integrations/types.d.ts +31 -0
  93. package/dist/integrations/types.d.ts.map +1 -1
  94. package/dist/integrations/types.js.map +1 -1
  95. package/dist/integrations/webhook-handler.js +1 -0
  96. package/dist/integrations/webhook-handler.js.map +1 -1
  97. package/dist/mcp/build-server.d.ts +9 -2
  98. package/dist/mcp/build-server.d.ts.map +1 -1
  99. package/dist/mcp/build-server.js +97 -20
  100. package/dist/mcp/build-server.js.map +1 -1
  101. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  102. package/dist/mcp/builtin-tools.js +5 -2
  103. package/dist/mcp/builtin-tools.js.map +1 -1
  104. package/dist/mcp/external-agent-policy.d.ts +19 -0
  105. package/dist/mcp/external-agent-policy.d.ts.map +1 -0
  106. package/dist/mcp/external-agent-policy.js +2 -0
  107. package/dist/mcp/external-agent-policy.js.map +1 -0
  108. package/dist/mcp/index.d.ts +1 -0
  109. package/dist/mcp/index.d.ts.map +1 -1
  110. package/dist/mcp/index.js.map +1 -1
  111. package/dist/notifications/routes.d.ts +1 -1
  112. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  113. package/dist/server/agent-chat/plugin-options.d.ts +11 -1
  114. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  115. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  116. package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
  117. package/dist/server/agent-chat/script-entries.js +16 -2
  118. package/dist/server/agent-chat/script-entries.js.map +1 -1
  119. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  120. package/dist/server/agent-chat-plugin.js +3 -0
  121. package/dist/server/agent-chat-plugin.js.map +1 -1
  122. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
  123. package/docs/content/external-agents.mdx +66 -7
  124. package/docs/content/locales/ar-SA/external-agents.mdx +40 -7
  125. package/docs/content/locales/de-DE/external-agents.mdx +41 -7
  126. package/docs/content/locales/es-ES/external-agents.mdx +41 -7
  127. package/docs/content/locales/fr-FR/external-agents.mdx +41 -7
  128. package/docs/content/locales/hi-IN/external-agents.mdx +40 -7
  129. package/docs/content/locales/ja-JP/external-agents.mdx +40 -7
  130. package/docs/content/locales/ko-KR/external-agents.mdx +40 -7
  131. package/docs/content/locales/pt-BR/external-agents.mdx +41 -7
  132. package/docs/content/locales/zh-CN/external-agents.mdx +39 -7
  133. package/docs/content/locales/zh-TW/external-agents.mdx +39 -7
  134. package/package.json +1 -1
  135. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
@@ -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
- if (verifySessionReplayAgentAccess(recordingId, token)) return token;
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(recordingId);
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");
@@ -2,12 +2,12 @@
2
2
  * The deliberately small authenticated MCP surface for Analytics.
3
3
  *
4
4
  * Keep this list read-only and incident-focused. In particular, do not add
5
- * replay-event/blob actions or dashboard/data mutation actions here.
5
+ * raw replay-event/blob actions or dashboard/data mutation actions here.
6
6
  */
7
7
  export const ANALYTICS_CONNECTOR_CATALOG = [
8
8
  "list-session-recordings",
9
9
  "get-session-replay-summary",
10
- "get-session-replay-events",
10
+ "get-session-replay-timeline",
11
11
  "query-agent-native-analytics",
12
12
  "list-error-issues",
13
13
  "get-error-issue",
@@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url";
18
18
  * reads go through `accessFilter` so an org-scoped key surfaces its issues to
19
19
  * the whole org exactly like session recordings.
20
20
  */
21
- import { readAppState } from "@agent-native/core/application-state";
21
+ import { appStateGet } from "@agent-native/core/application-state";
22
22
  import { notifyWithDelivery } from "@agent-native/core/notifications";
23
23
  import { recordChange } from "@agent-native/core/server";
24
24
  import { accessFilter } from "@agent-native/core/sharing";
@@ -26,6 +26,7 @@ import {
26
26
  and,
27
27
  desc,
28
28
  eq,
29
+ exists,
29
30
  inArray,
30
31
  isNull,
31
32
  notInArray,
@@ -1073,6 +1074,8 @@ export interface ListErrorIssuesFilters {
1073
1074
  status?: IssueStatus | "all";
1074
1075
  query?: string;
1075
1076
  app?: string;
1077
+ sessionRecordingId?: string;
1078
+ userId?: string;
1076
1079
  sort?: "lastSeen" | "eventCount" | "firstSeen";
1077
1080
  limit?: number;
1078
1081
  }
@@ -1106,10 +1109,14 @@ export function anonymizeErrorReportingEmails<T>(value: T): T {
1106
1109
  return value;
1107
1110
  }
1108
1111
 
1109
- async function isErrorDemoModeEnabled(): Promise<boolean> {
1112
+ async function isErrorDemoModeEnabled(
1113
+ userEmail?: string | null,
1114
+ ): Promise<boolean> {
1110
1115
  if (process.env.DEMO_MODE === "true") return true;
1116
+ if (!userEmail) return false;
1111
1117
  try {
1112
- const state = await readAppState("demo-mode");
1118
+ // Keep demo state tied to the caller whose issues are being read.
1119
+ const state = await appStateGet(userEmail, "demo-mode");
1113
1120
  return state?.enabled === true;
1114
1121
  } catch {
1115
1122
  return false;
@@ -1195,7 +1202,7 @@ export async function listErrorIssues(
1195
1202
  scope: ErrorReadScope,
1196
1203
  filters: ListErrorIssuesFilters = {},
1197
1204
  ): Promise<ErrorIssueSummary[]> {
1198
- const demoModeEnabled = isErrorDemoModeEnabled();
1205
+ const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1199
1206
  const db = getDb() as any;
1200
1207
  const limit = Math.min(
1201
1208
  MAX_ISSUE_LIMIT,
@@ -1206,6 +1213,41 @@ export async function listErrorIssues(
1206
1213
  conditions.push(eq(schema.errorIssues.status, filters.status));
1207
1214
  }
1208
1215
  if (filters.app) conditions.push(eq(schema.errorIssues.app, filters.app));
1216
+ const sessionRecordingId = filters.sessionRecordingId?.trim();
1217
+ const userId = filters.userId?.trim();
1218
+ if (sessionRecordingId || userId) {
1219
+ const occurrenceConditions: any[] = [
1220
+ eq(schema.errorEvents.issueId, schema.errorIssues.id),
1221
+ // Keep the child occurrence in the same tenant as its parent issue even
1222
+ // when the issue is visible through an org share.
1223
+ eq(schema.errorEvents.ownerEmail, schema.errorIssues.ownerEmail),
1224
+ or(
1225
+ eq(schema.errorEvents.orgId, schema.errorIssues.orgId),
1226
+ and(isNull(schema.errorEvents.orgId), isNull(schema.errorIssues.orgId)),
1227
+ ),
1228
+ ];
1229
+ if (sessionRecordingId) {
1230
+ occurrenceConditions.push(
1231
+ eq(schema.errorEvents.sessionRecordingId, sessionRecordingId),
1232
+ );
1233
+ }
1234
+ if (userId) {
1235
+ occurrenceConditions.push(
1236
+ or(
1237
+ eq(schema.errorEvents.userId, userId),
1238
+ eq(schema.errorEvents.userKey, userId),
1239
+ ),
1240
+ );
1241
+ }
1242
+ conditions.push(
1243
+ exists(
1244
+ db
1245
+ .select({ id: schema.errorEvents.id })
1246
+ .from(schema.errorEvents)
1247
+ .where(and(...occurrenceConditions)),
1248
+ ),
1249
+ );
1250
+ }
1209
1251
  const query = filters.query?.trim();
1210
1252
  if (query) {
1211
1253
  conditions.push(
@@ -1343,7 +1385,7 @@ export async function getErrorIssue(
1343
1385
  issueId: string,
1344
1386
  options: { eventsLimit?: number } = {},
1345
1387
  ): Promise<ErrorIssueDetail> {
1346
- const demoModeEnabled = isErrorDemoModeEnabled();
1388
+ const demoModeEnabled = isErrorDemoModeEnabled(scope.userEmail);
1347
1389
  const db = getDb() as any;
1348
1390
  const [issueRow] = await db
1349
1391
  .select()
@@ -22,6 +22,7 @@ import {
22
22
  } from "../../shared/session-replay-diagnostics.js";
23
23
  import {
24
24
  compactSessionRecordingSummary,
25
+ getSessionReplayEvents,
25
26
  getSessionReplaySummary,
26
27
  getSessionReplayTokenizedEvents,
27
28
  getSessionReplayTokenizedSummary,
@@ -82,20 +83,16 @@ function replayStartedAt(events: AgentReplayEvent[]): number {
82
83
 
83
84
  function pathLabel(href: string): string {
84
85
  try {
85
- const parsed = new URL(href);
86
- return parsed.pathname || parsed.hostname;
86
+ const parsed = new URL(href, "https://placeholder.agent-native.local");
87
+ return boundedDiagnosticText(
88
+ parsed.pathname || parsed.hostname,
89
+ MAX_DIAGNOSTIC_URL_CHARS,
90
+ );
87
91
  } catch {
88
- return href;
92
+ return boundedDiagnosticText(href, MAX_DIAGNOSTIC_URL_CHARS);
89
93
  }
90
94
  }
91
95
 
92
- function markerDetail(event: AgentReplayEvent): string | null {
93
- const payload = event.data?.payload;
94
- if (typeof payload?.message === "string") return payload.message;
95
- if (typeof event.data?.href === "string") return event.data.href;
96
- return null;
97
- }
98
-
99
96
  const TIMELINE_MARKER_CAP = 200;
100
97
  const TIMELINE_ERROR_MARKER_RESERVE = 100;
101
98
  const MAX_DIAGNOSTIC_MESSAGE_CHARS = 1_000;
@@ -528,10 +525,9 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
528
525
  if (isFailedSessionReplayNetworkStatus(status)) {
529
526
  const method =
530
527
  boundedDiagnosticText(tagged.payload.method, 16) || "GET";
531
- const url = boundedDiagnosticText(
532
- tagged.payload.url,
533
- MAX_DIAGNOSTIC_URL_CHARS,
534
- );
528
+ // External-agent timelines intentionally expose only a path label.
529
+ // Query strings commonly contain emails, tokens, and other secrets.
530
+ const url = pathLabel(String(tagged.payload.url ?? ""));
535
531
  markers.push({
536
532
  timestamp,
537
533
  offsetMs: Math.max(0, timestamp - startedAt),
@@ -553,7 +549,7 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
553
549
  offsetMs: Math.max(0, timestamp - startedAt),
554
550
  kind: "navigation",
555
551
  label: pathLabel(event.data.href),
556
- detail: event.data.href,
552
+ detail: pathLabel(event.data.href),
557
553
  });
558
554
  } else if (
559
555
  event.type === RRWEB_EVENT_TYPE.IncrementalSnapshot &&
@@ -606,8 +602,14 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
606
602
  timestamp,
607
603
  offsetMs: Math.max(0, timestamp - startedAt),
608
604
  kind: "custom",
609
- label: String(event.data?.tag ?? "Custom event"),
610
- detail: markerDetail(event),
605
+ label: boundedDiagnosticText(
606
+ String(event.data?.tag ?? "Custom event"),
607
+ MAX_DIAGNOSTIC_MESSAGE_CHARS,
608
+ ),
609
+ // Custom-event payloads are application-defined and can contain input
610
+ // values or secrets. The tag and timestamp are sufficient metadata for
611
+ // this sanitized external-agent timeline.
612
+ detail: null,
611
613
  });
612
614
  }
613
615
  }
@@ -615,14 +617,56 @@ function buildReplayTimeline(events: AgentReplayEvent[]) {
615
617
  return capReplayTimelineMarkers(markers);
616
618
  }
617
619
 
620
+ export async function getSessionReplayTimeline(
621
+ recordingId: string,
622
+ scope: ReplayScope,
623
+ options: { eventLimit?: number } = {},
624
+ ) {
625
+ const eventLimit = Math.min(
626
+ 10_000,
627
+ Math.max(1, Math.floor(options.eventLimit ?? 10_000)),
628
+ );
629
+ const eventsResponse = await getSessionReplayEvents(recordingId, scope, {
630
+ limit: eventLimit,
631
+ });
632
+ const events = eventsResponse.chunks.flatMap((chunk) =>
633
+ chunk.events.filter(
634
+ (event): event is AgentReplayEvent =>
635
+ Boolean(event) && typeof event === "object",
636
+ ),
637
+ );
638
+ const markers = buildReplayTimeline(events);
639
+
640
+ return {
641
+ recording: compactSessionRecordingSummary(eventsResponse.recording),
642
+ markerCount: markers.length,
643
+ markers,
644
+ eventCount: eventsResponse.eventCount,
645
+ truncated: eventsResponse.truncated,
646
+ unavailableChunks: eventsResponse.unavailableChunks,
647
+ };
648
+ }
649
+
618
650
  export function verifySessionReplayAgentAccess(
619
651
  recordingId: string,
620
652
  token: string,
621
653
  ): boolean {
622
- return verifyScopedAgentAccessToken(token, {
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, {
623
662
  resourceKind: SESSION_REPLAY_AGENT_ACCESS_TOKEN_PREFIX,
624
663
  resourceId: recordingId,
625
- }).ok;
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 };
626
670
  }
627
671
 
628
672
  export async function createSessionReplayAgentLink({
@@ -675,14 +719,18 @@ export async function buildSessionReplayAgentContext({
675
719
  origin?: string;
676
720
  includeTimeline?: boolean;
677
721
  }) {
678
- if (!verifySessionReplayAgentAccess(recordingId, token)) {
722
+ const access = resolveSessionReplayAgentAccess(recordingId, token);
723
+ if (!access) {
679
724
  const error = Object.assign(new Error("Invalid or expired agent access"), {
680
725
  statusCode: 401,
681
726
  });
682
727
  throw error;
683
728
  }
684
729
 
685
- const recording = await getSessionReplayTokenizedSummary(recordingId);
730
+ const recording = await getSessionReplayTokenizedSummary(
731
+ recordingId,
732
+ access.viewerEmail,
733
+ );
686
734
  const resolvedOrigin = appOrigin(origin);
687
735
  const basePath = appBasePath();
688
736
  const contextUrl = buildAgentAccessApiUrl({
@@ -715,7 +763,9 @@ export async function buildSessionReplayAgentContext({
715
763
  });
716
764
 
717
765
  const eventsResponse = includeTimeline
718
- ? await getSessionReplayTokenizedEvents(recording.id, { limit: 10000 })
766
+ ? await getSessionReplayTokenizedEvents(recording.id, access.viewerEmail, {
767
+ limit: 10000,
768
+ })
719
769
  : null;
720
770
  const events =
721
771
  eventsResponse?.chunks.flatMap((chunk) =>
@@ -2,14 +2,18 @@ import { Buffer } from "node:buffer";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import { gzipSync, gunzipSync } from "node:zlib";
4
4
 
5
- import { readAppState } from "@agent-native/core/application-state";
5
+ import { appStateGet } from "@agent-native/core/application-state";
6
6
  import {
7
7
  deletePrivateBlob,
8
8
  putPrivateBlob,
9
9
  readPrivateBlob,
10
10
  type PrivateBlobHandle,
11
11
  } from "@agent-native/core/private-blob";
12
- import { recordChange, runWithRequestContext } from "@agent-native/core/server";
12
+ import {
13
+ getRequestUserEmail,
14
+ recordChange,
15
+ runWithRequestContext,
16
+ } from "@agent-native/core/server";
13
17
  import {
14
18
  accessFilter,
15
19
  resolveAccess,
@@ -301,10 +305,15 @@ function isBuilderEmail(value: unknown): boolean {
301
305
  return normalizedEmail(value)?.endsWith("@builder.io") === true;
302
306
  }
303
307
 
304
- async function isSessionDemoModeEnabled(): Promise<boolean> {
308
+ async function isSessionDemoModeEnabled(
309
+ userEmail?: string | null,
310
+ ): Promise<boolean> {
305
311
  if (process.env.DEMO_MODE === "true") return true;
312
+ if (!userEmail) return false;
306
313
  try {
307
- const state = await readAppState("demo-mode");
314
+ // These reads also run from custom routes and actions, so use the
315
+ // authenticated scope explicitly instead of ambient request state.
316
+ const state = await appStateGet(userEmail, "demo-mode");
308
317
  return state?.enabled === true;
309
318
  } catch {
310
319
  return false;
@@ -1677,7 +1686,7 @@ export async function listSessionRecordings(
1677
1686
  filters: SessionReplayListFilters = {},
1678
1687
  ): Promise<SessionRecordingSummary[]> {
1679
1688
  const db = getDb() as any;
1680
- const demoMode = await isSessionDemoModeEnabled();
1689
+ const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
1681
1690
  const limit = Math.min(
1682
1691
  MAX_SESSION_RECORDINGS_LIMIT,
1683
1692
  Math.max(1, filters.limit ?? DEFAULT_SESSION_RECORDINGS_LIMIT),
@@ -1759,7 +1768,7 @@ export async function getSessionReplaySummary(
1759
1768
  recordingId: string,
1760
1769
  scope: SessionReplayScope,
1761
1770
  ): Promise<SessionRecordingSummary> {
1762
- const demoMode = await isSessionDemoModeEnabled();
1771
+ const demoMode = await isSessionDemoModeEnabled(scope.userEmail);
1763
1772
  const access = await resolveAccess("session-recording", recordingId, {
1764
1773
  userEmail: scope.userEmail,
1765
1774
  orgId: scope.orgId ?? undefined,
@@ -1777,8 +1786,13 @@ export async function getSessionReplaySummary(
1777
1786
 
1778
1787
  export async function getSessionReplayTokenizedSummary(
1779
1788
  recordingId: string,
1789
+ viewerEmail: string,
1780
1790
  ): Promise<SessionRecordingSummary> {
1781
- const demoMode = await isSessionDemoModeEnabled();
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);
1782
1796
  const db = getDb() as any;
1783
1797
  // guard:allow-unscoped -- called only after verifySessionReplayAgentAccess(recordingId, token) verifies a signed, recording-scoped agent_access token.
1784
1798
  const [row] = await db
@@ -1838,6 +1852,7 @@ export async function getSessionReplayManifest(
1838
1852
 
1839
1853
  export async function getSessionReplayTokenizedManifest(
1840
1854
  recordingId: string,
1855
+ viewerEmail: string,
1841
1856
  ): Promise<{
1842
1857
  recording: AgentSessionRecordingSummary;
1843
1858
  chunks: Array<{
@@ -1850,7 +1865,10 @@ export async function getSessionReplayTokenizedManifest(
1850
1865
  bytesPath: string;
1851
1866
  }>;
1852
1867
  }> {
1853
- const recording = await getSessionReplayTokenizedSummary(recordingId);
1868
+ const recording = await getSessionReplayTokenizedSummary(
1869
+ recordingId,
1870
+ viewerEmail,
1871
+ );
1854
1872
  const manifest = await getSessionReplayManifestForRecording(recording);
1855
1873
  return {
1856
1874
  ...manifest,
@@ -1914,6 +1932,7 @@ export async function readSessionReplayChunkBytes(
1914
1932
  export async function readSessionReplayTokenizedChunkBytes(
1915
1933
  recordingId: string,
1916
1934
  seq: number,
1935
+ viewerEmail: string,
1917
1936
  ): Promise<{
1918
1937
  recording: AgentSessionRecordingSummary;
1919
1938
  seq: number;
@@ -1921,7 +1940,10 @@ export async function readSessionReplayTokenizedChunkBytes(
1921
1940
  /** Decompressed replay-chunk JSON text (a serialized rrweb events array). */
1922
1941
  json: string;
1923
1942
  }> {
1924
- const recording = await getSessionReplayTokenizedSummary(recordingId);
1943
+ const recording = await getSessionReplayTokenizedSummary(
1944
+ recordingId,
1945
+ viewerEmail,
1946
+ );
1925
1947
  const chunk = await readSessionReplayChunkBytesForRecording(recording, seq);
1926
1948
  return {
1927
1949
  ...chunk,
@@ -2166,8 +2188,12 @@ export async function readSessionReplayChunkBatch(
2166
2188
  export async function readSessionReplayTokenizedChunkBatch(
2167
2189
  recordingId: string,
2168
2190
  seqs: number[],
2191
+ viewerEmail: string,
2169
2192
  ): Promise<SessionReplayChunkBatchResult> {
2170
- const recording = await getSessionReplayTokenizedSummary(recordingId);
2193
+ const recording = await getSessionReplayTokenizedSummary(
2194
+ recordingId,
2195
+ viewerEmail,
2196
+ );
2171
2197
  return readSessionReplayChunkBatchForRecording(recording, seqs);
2172
2198
  }
2173
2199
 
@@ -2195,6 +2221,7 @@ export async function getSessionReplayEvents(
2195
2221
 
2196
2222
  export async function getSessionReplayTokenizedEvents(
2197
2223
  recordingId: string,
2224
+ viewerEmail: string,
2198
2225
  options: SessionReplayEventReadOptions = {},
2199
2226
  ): Promise<{
2200
2227
  recording: AgentSessionRecordingSummary;
@@ -2210,7 +2237,10 @@ export async function getSessionReplayTokenizedEvents(
2210
2237
  truncated: boolean;
2211
2238
  unavailableChunks: number;
2212
2239
  }> {
2213
- const recording = await getSessionReplayTokenizedSummary(recordingId);
2240
+ const recording = await getSessionReplayTokenizedSummary(
2241
+ recordingId,
2242
+ viewerEmail,
2243
+ );
2214
2244
  const result = await getSessionReplayEventsForRecording(recording, options);
2215
2245
  return {
2216
2246
  ...result,
@@ -258,6 +258,13 @@ export default createAgentChatPlugin({
258
258
  // AGENT_PROD_CODE_EXECUTION=trusted to also enable bash/read/edit/write.
259
259
  codeExecution: { production: "sandboxed" },
260
260
  connectorCatalog: [...ANALYTICS_CONNECTOR_CATALOG],
261
+ externalAgents: {
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",
266
+ writes: "ask_app_only",
267
+ },
261
268
  resolveOrgId: async (event) => {
262
269
  const ctx = await getOrgContext(event);
263
270
  return ctx.orgId;
@@ -1249,6 +1249,21 @@ const runAnalyticsMigrations = runMigrations(
1249
1249
  sqlite: `ALTER TABLE monitor_incidents ADD COLUMN IF NOT EXISTS notification_delivered INTEGER NOT NULL DEFAULT 0`,
1250
1250
  },
1251
1251
  },
1252
+ {
1253
+ version: 118,
1254
+ name: "error-events-session-recording-filter-idx",
1255
+ sql: `CREATE INDEX IF NOT EXISTS error_events_session_recording_filter_idx ON error_events (session_recording_id, owner_email, org_id, issue_id)`,
1256
+ },
1257
+ {
1258
+ version: 119,
1259
+ name: "error-events-user-id-filter-idx",
1260
+ sql: `CREATE INDEX IF NOT EXISTS error_events_user_id_filter_idx ON error_events (user_id, owner_email, org_id, issue_id)`,
1261
+ },
1262
+ {
1263
+ version: 120,
1264
+ name: "error-events-user-key-filter-idx",
1265
+ sql: `CREATE INDEX IF NOT EXISTS error_events_user_key_filter_idx ON error_events (user_key, owner_email, org_id, issue_id)`,
1266
+ },
1252
1267
  ],
1253
1268
  { table: "analytics_migrations" },
1254
1269
  );
@@ -8,8 +8,8 @@ import {
8
8
  import { type SessionReplayConsoleLevel } from "../../../../shared/session-replay-diagnostics.js";
9
9
  import {
10
10
  buildSessionReplayDiagnostics,
11
+ resolveSessionReplayAgentAccess,
11
12
  SESSION_REPLAY_AGENT_ACCESS_PARAM,
12
- verifySessionReplayAgentAccess,
13
13
  } from "../../../lib/session-replay-agent-context.js";
14
14
  import { getSessionReplayTokenizedEvents } from "../../../lib/session-replay.js";
15
15
 
@@ -48,7 +48,8 @@ export default defineEventHandler(async (event) => {
48
48
  setResponseStatus(event, 400);
49
49
  return { error: "id and agent access token are required" };
50
50
  }
51
- if (!verifySessionReplayAgentAccess(id, token)) {
51
+ const access = resolveSessionReplayAgentAccess(id, token);
52
+ if (!access) {
52
53
  setResponseStatus(event, 401);
53
54
  return { error: "Invalid or expired agent access" };
54
55
  }
@@ -108,9 +109,11 @@ export default defineEventHandler(async (event) => {
108
109
  : (rawOffset ? offset : 0) + limit,
109
110
  ),
110
111
  );
111
- const eventsResponse = await getSessionReplayTokenizedEvents(id, {
112
- limit: replayEventReadLimit,
113
- });
112
+ const eventsResponse = await getSessionReplayTokenizedEvents(
113
+ id,
114
+ access.viewerEmail,
115
+ { limit: replayEventReadLimit },
116
+ );
114
117
  const events = eventsResponse.chunks.flatMap((chunk) =>
115
118
  chunk.events.filter(
116
119
  (item): item is Record<string, any> =>
@@ -6,8 +6,8 @@ import {
6
6
  } from "h3";
7
7
 
8
8
  import {
9
+ resolveSessionReplayAgentAccess,
9
10
  SESSION_REPLAY_AGENT_ACCESS_PARAM,
10
- verifySessionReplayAgentAccess,
11
11
  } from "../../../lib/session-replay-agent-context.js";
12
12
  import {
13
13
  getSessionReplayTokenizedEvents,
@@ -43,7 +43,8 @@ export default defineEventHandler(async (event) => {
43
43
  setResponseStatus(event, 400);
44
44
  return { error: "id and agent access token are required" };
45
45
  }
46
- if (!verifySessionReplayAgentAccess(id, token)) {
46
+ const access = resolveSessionReplayAgentAccess(id, token);
47
+ if (!access) {
47
48
  setResponseStatus(event, 401);
48
49
  return { error: "Invalid or expired agent access" };
49
50
  }
@@ -55,7 +56,11 @@ export default defineEventHandler(async (event) => {
55
56
  };
56
57
 
57
58
  try {
58
- return await getSessionReplayTokenizedEvents(id, options);
59
+ return await getSessionReplayTokenizedEvents(
60
+ id,
61
+ access.viewerEmail,
62
+ options,
63
+ );
59
64
  } catch (error: any) {
60
65
  setResponseStatus(event, error?.statusCode ?? 400);
61
66
  return { error: error?.message || String(error) };
@@ -37,6 +37,7 @@ export function DeleteEventDialog({
37
37
  }: DeleteEventDialogProps) {
38
38
  const t = useT();
39
39
  const confirmButtonRef = useRef<HTMLButtonElement>(null);
40
+ const dontNotifyButtonRef = useRef<HTMLButtonElement>(null);
40
41
  const [scope, setScope] = useState<DeleteEventScope>("single");
41
42
  const [message, setMessage] = useState("");
42
43
 
@@ -114,7 +115,12 @@ export function DeleteEventDialog({
114
115
  onKeyDown={handleKeyDown}
115
116
  onOpenAutoFocus={(event) => {
116
117
  event.preventDefault();
117
- confirmButtonRef.current?.focus();
118
+ // For events with guests, Enter should default to the reversible,
119
+ // no-email path instead of immediately sending a cancellation.
120
+ (canNotifyGuests
121
+ ? dontNotifyButtonRef.current
122
+ : confirmButtonRef.current
123
+ )?.focus();
118
124
  }}
119
125
  >
120
126
  <AlertDialogHeader>
@@ -177,7 +183,11 @@ export function DeleteEventDialog({
177
183
  {t("deleteEvent.cancel")}
178
184
  </AlertDialogCancel>
179
185
  {canNotifyGuests && (
180
- <Button variant="outline" onClick={() => handleConfirm("none")}>
186
+ <Button
187
+ ref={dontNotifyButtonRef}
188
+ variant="outline"
189
+ onClick={() => handleConfirm("none")}
190
+ >
181
191
  {t("deleteEvent.dontNotify")}
182
192
  </Button>
183
193
  )}