@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +18 -0
- package/corpus/core/docs/content/external-agents.mdx +66 -7
- package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +40 -7
- package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +41 -7
- package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +41 -7
- package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +41 -7
- package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +40 -7
- package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +40 -7
- package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +40 -7
- package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +41 -7
- package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +39 -7
- package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +39 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/handlers.ts +101 -26
- package/corpus/core/src/a2a/task-store.ts +128 -3
- package/corpus/core/src/client/session-replay.ts +67 -5
- package/corpus/core/src/integrations/adapters/slack.ts +207 -17
- package/corpus/core/src/integrations/identity-links-store.ts +210 -0
- package/corpus/core/src/integrations/identity.ts +196 -0
- package/corpus/core/src/integrations/index.ts +6 -0
- package/corpus/core/src/integrations/plugin.ts +130 -1
- package/corpus/core/src/integrations/types.ts +37 -0
- package/corpus/core/src/integrations/webhook-handler.ts +1 -0
- package/corpus/core/src/mcp/build-server.ts +127 -22
- package/corpus/core/src/mcp/builtin-tools.ts +5 -2
- package/corpus/core/src/mcp/external-agent-policy.ts +18 -0
- package/corpus/core/src/mcp/index.ts +1 -0
- package/corpus/core/src/server/agent-chat/plugin-options.ts +12 -1
- package/corpus/core/src/server/agent-chat/script-entries.ts +20 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +3 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +4 -1
- package/corpus/templates/analytics/AGENTS.md +20 -5
- package/corpus/templates/analytics/actions/create-session-replay-agent-link.ts +0 -1
- package/corpus/templates/analytics/actions/get-session-replay-events.ts +0 -1
- package/corpus/templates/analytics/actions/get-session-replay-timeline.ts +40 -0
- package/corpus/templates/analytics/actions/list-error-issues.ts +12 -0
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +4 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +48 -17
- 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/changelog/2026-07-12-connected-external-agents-can-now-look-up-sessions-error-iss.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-session-identities-stay-visible-when-demo-mode-is-off.md +6 -0
- package/corpus/templates/analytics/server/handlers/session-replay.ts +15 -9
- package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +2 -2
- package/corpus/templates/analytics/server/lib/error-capture.ts +47 -5
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +72 -22
- package/corpus/templates/analytics/server/lib/session-replay.ts +41 -11
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +7 -0
- package/corpus/templates/analytics/server/plugins/db.ts +15 -0
- 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/corpus/templates/forms/app/global.css +54 -8
- package/corpus/templates/forms/app/pages/AskPage.tsx +35 -13
- package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +11 -17
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +8 -5
- package/corpus/templates/forms/changelog/2026-07-12-ask-forms-suggestions-now-sit-beneath-the-composer-for-a-tig.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-12-builder-response-tables-now-fill-the-view-without-a-redundan.md +6 -0
- package/corpus/templates/forms/changelog/2026-07-12-response-tables-now-fill-the-available-pane-with-a-flexible-.md +6 -0
- package/corpus/templates/mail/AGENTS.md +3 -1
- package/corpus/templates/mail/server/lib/mail-integrations.ts +3 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +89 -20
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/task-store.d.ts +32 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +111 -4
- 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/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +176 -17
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/identity-links-store.d.ts +23 -0
- package/dist/integrations/identity-links-store.d.ts.map +1 -0
- package/dist/integrations/identity-links-store.js +158 -0
- package/dist/integrations/identity-links-store.js.map +1 -0
- package/dist/integrations/identity.d.ts +28 -0
- package/dist/integrations/identity.d.ts.map +1 -0
- package/dist/integrations/identity.js +142 -0
- package/dist/integrations/identity.js.map +1 -0
- package/dist/integrations/index.d.ts +2 -0
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +2 -0
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +93 -1
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/types.d.ts +31 -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/build-server.d.ts +9 -2
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +97 -20
- package/dist/mcp/build-server.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/mcp/external-agent-policy.d.ts +19 -0
- package/dist/mcp/external-agent-policy.d.ts.map +1 -0
- package/dist/mcp/external-agent-policy.js +2 -0
- package/dist/mcp/external-agent-policy.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/server/agent-chat/plugin-options.d.ts +11 -1
- package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
- package/dist/server/agent-chat/plugin-options.js.map +1 -1
- package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
- package/dist/server/agent-chat/script-entries.js +16 -2
- package/dist/server/agent-chat/script-entries.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +3 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +34 -5
- package/docs/content/external-agents.mdx +66 -7
- package/docs/content/locales/ar-SA/external-agents.mdx +40 -7
- package/docs/content/locales/de-DE/external-agents.mdx +41 -7
- package/docs/content/locales/es-ES/external-agents.mdx +41 -7
- package/docs/content/locales/fr-FR/external-agents.mdx +41 -7
- package/docs/content/locales/hi-IN/external-agents.mdx +40 -7
- package/docs/content/locales/ja-JP/external-agents.mdx +40 -7
- package/docs/content/locales/ko-KR/external-agents.mdx +40 -7
- package/docs/content/locales/pt-BR/external-agents.mdx +41 -7
- package/docs/content/locales/zh-CN/external-agents.mdx +39 -7
- package/docs/content/locales/zh-TW/external-agents.mdx +39 -7
- package/package.json +1 -1
- 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
|
-
|
|
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");
|
|
@@ -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-
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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:
|
|
610
|
-
|
|
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
|
|
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
|
-
})
|
|
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
|
-
|
|
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(
|
|
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, {
|
|
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 {
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
);
|
package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
112
|
-
|
|
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> =>
|
package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
186
|
+
<Button
|
|
187
|
+
ref={dontNotifyButtonRef}
|
|
188
|
+
variant="outline"
|
|
189
|
+
onClick={() => handleConfirm("none")}
|
|
190
|
+
>
|
|
181
191
|
{t("deleteEvent.dontNotify")}
|
|
182
192
|
</Button>
|
|
183
193
|
)}
|