@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
@@ -51,6 +51,7 @@ import {
51
51
  MCP_CONNECT_SCOPE,
52
52
  } from "./connect-store.js";
53
53
  import { MCP_APP_REQUEST_ORIGIN_CSP_SOURCE } from "./embed-app.js";
54
+ import type { ExternalAgentPolicy } from "./external-agent-policy.js";
54
55
  import {
55
56
  MCP_OAUTH_SCOPES,
56
57
  hasMcpOAuthScope,
@@ -124,8 +125,9 @@ export interface MCPConfig {
124
125
  * from connectors. It is no longer gated behind an environment variable, and
125
126
  * the catalog is never inferred from the client name/user-agent.
126
127
  *
127
- * `tool-search` stays available in the compact catalog so any trimmed tool is
128
- * reachable on demand. Callers who need the full surface up front opt in
128
+ * `tool-search` stays available in the compact catalog for discovery. A
129
+ * searched action still needs the connector catalog or authenticated-read
130
+ * policy before `tools/call`; callers who need the full surface up front opt in
129
131
  * explicitly with `agent-native connect --full-catalog` (embeds a
130
132
  * `catalog_scope: "full"` claim in the connect-minted JWT) or the
131
133
  * deployment-wide `AGENT_NATIVE_MCP_FULL_CATALOG=1` env override.
@@ -134,6 +136,11 @@ export interface MCPConfig {
134
136
  * setting it on `MCPConfig` directly; the plugin copies it through.
135
137
  */
136
138
  connectorCatalog?: string[];
139
+ /**
140
+ * Optional policy for automatically exposing explicitly annotated,
141
+ * authenticated read actions to external MCP callers.
142
+ */
143
+ externalAgents?: ExternalAgentPolicy;
137
144
  }
138
145
 
139
146
  /**
@@ -281,7 +288,7 @@ function explicitlyRequestsFullMcpCatalog(
281
288
  // (100k+ tokens) into a context window just because a client called itself
282
289
  // "code"/"cursor"/"codex" was a recurring footgun. Everything else gets the
283
290
  // connector/compact catalog plus `tool-search`, which keeps every tool
284
- // reachable on demand.
291
+ // discoverable; only permitted actions are callable without full opt-in.
285
292
  if (process.env.AGENT_NATIVE_MCP_FULL_CATALOG === "1") return true;
286
293
  return requestMeta?.fullCatalog === true;
287
294
  }
@@ -309,17 +316,101 @@ function warnFullCatalogServed(toolCount: number): void {
309
316
  );
310
317
  }
311
318
 
319
+ function isAuthenticatedReadAction(entry: ActionEntry): boolean {
320
+ return (
321
+ entry.http !== false &&
322
+ entry.http?.method === "GET" &&
323
+ entry.readOnly === true &&
324
+ entry.publicAgent?.expose === true &&
325
+ entry.publicAgent.readOnly === true &&
326
+ entry.publicAgent.requiresAuth === true
327
+ );
328
+ }
329
+
312
330
  /**
313
- * Returns true when the given action name is in the template's connector
314
- * catalog, OR is a builtin cross-app tool that is always included for
315
- * external connector clients. Builtin tool names from
316
- * `COMPACT_MCP_APP_CATALOG_BUILTINS` are always allowed since they are the
317
- * stable external-agent verb set.
331
+ * Hard exclusion list for the `authenticatedReads: "auto"` derivation ONLY
332
+ * (see `autoAuthenticatedReadNames` below). Explicit `connectorCatalog`
333
+ * entries are a deliberate, reviewed choice made by the app and are NOT
334
+ * affected by this list an app can still list any of these names in
335
+ * `connectorCatalog` on purpose.
336
+ *
337
+ * These are the footgun families this file's other comments already call
338
+ * out ("removes footguns (db-exec, seed-*, extension tools, browser-session
339
+ * tools, etc.)" above, and "keeps db-exec / seed-* / extension /
340
+ * browser-session footguns off the external surface" near the connector
341
+ * tier below): generic core SQL access, template demo/seed data, the
342
+ * extension-management suite, live browser-session control, and Context
343
+ * X-Ray internals. `isAuthenticatedReadAction` only inspects action
344
+ * metadata (http/readOnly/publicAgent flags) — nothing stops a future
345
+ * change from mis-annotating one of these with that exact flag set again,
346
+ * the way `db-query`/`db-schema` were briefly (and accidentally) annotated
347
+ * before it was caught in review. These names can never be auto-derived
348
+ * from metadata alone; exposing one to external callers requires an
349
+ * explicit `connectorCatalog` entry.
318
350
  */
319
- function isActionInConnectorCatalog(name: string, config: MCPConfig): boolean {
320
- if (COMPACT_MCP_APP_CATALOG_BUILTINS.has(name)) return true;
321
- if (!Array.isArray(config.connectorCatalog)) return false;
322
- return config.connectorCatalog.includes(name);
351
+ const AUTO_READ_EXCLUDED_ACTION_NAMES = new Set([
352
+ "db-query",
353
+ "db-schema",
354
+ "db-exec",
355
+ "db-patch",
356
+ "context-manifest-get",
357
+ "context-pin",
358
+ "context-evict",
359
+ "context-restore",
360
+ "context-report",
361
+ ]);
362
+
363
+ /**
364
+ * Substring/prefix patterns for excluded name *families* that aren't a
365
+ * fixed, enumerable set: `seed-*` varies per app/template, and the
366
+ * extension-management and browser-session tool suites use varying verb
367
+ * prefixes around a shared noun (e.g. `list-extensions`, `create-extension`,
368
+ * `hide-extension`; `list-browser-sessions`, `run-browser-session-action`) —
369
+ * so a leading-prefix match alone would miss most of them.
370
+ */
371
+ const AUTO_READ_EXCLUDED_ACTION_PATTERNS: RegExp[] = [
372
+ /^seed-/,
373
+ /extension/,
374
+ /browser-session/,
375
+ ];
376
+
377
+ function isAutoReadExcludedActionName(name: string): boolean {
378
+ return (
379
+ AUTO_READ_EXCLUDED_ACTION_NAMES.has(name) ||
380
+ AUTO_READ_EXCLUDED_ACTION_PATTERNS.some((pattern) => pattern.test(name))
381
+ );
382
+ }
383
+
384
+ function autoAuthenticatedReadNames(
385
+ actions: Record<string, ActionEntry>,
386
+ config: MCPConfig,
387
+ ): Set<string> {
388
+ if (config.externalAgents?.authenticatedReads !== "auto") return new Set();
389
+ return new Set(
390
+ Object.entries(actions)
391
+ .filter(
392
+ ([name, entry]) =>
393
+ isAuthenticatedReadAction(entry) &&
394
+ !isAutoReadExcludedActionName(name),
395
+ )
396
+ .map(([name]) => name),
397
+ );
398
+ }
399
+
400
+ function externalAgentDenySet(config: MCPConfig): Set<string> {
401
+ return new Set(
402
+ (config.externalAgents?.denyActions ?? [])
403
+ .map((name) => name.trim())
404
+ .filter(Boolean),
405
+ );
406
+ }
407
+
408
+ function externalAgentWritesAreAskAppOnly(config: MCPConfig): boolean {
409
+ return (
410
+ config.externalAgents?.writes === "ask_app_only" ||
411
+ (config.externalAgents?.authenticatedReads === "auto" &&
412
+ config.externalAgents?.writes !== "allowlisted")
413
+ );
323
414
  }
324
415
 
325
416
  interface ResolvedMcpAppResource {
@@ -1300,16 +1391,21 @@ export async function createMCPServerForRequest(
1300
1391
  ),
1301
1392
  )
1302
1393
  : visibleActions;
1394
+ const autoReadNames = autoAuthenticatedReadNames(visibleActions, config);
1395
+ const connectorNames = new Set([
1396
+ ...(config.connectorCatalog ?? []),
1397
+ ...autoReadNames,
1398
+ ]);
1399
+ const denyNames = externalAgentDenySet(config);
1400
+ const automaticConnectorPolicyActive =
1401
+ config.externalAgents?.authenticatedReads === "auto";
1303
1402
  // Connector-catalog tier: when a template declares a connector allow-list,
1304
- // serve exactly that curated surface (+ cross-app builtins + tool-search) to
1305
- // external callers unless they explicitly opted into the full catalog. This
1306
- // is active by default whenever a catalog is declared no env flag required —
1307
- // so the ~105-tool full catalog can never leak just because a deployment
1308
- // forgot to set one. It also keeps db-exec / seed-* / extension /
1403
+ // serve exactly that curated surface plus any explicitly annotated
1404
+ // authenticated reads from `externalAgents.authenticatedReads: "auto"`.
1405
+ // This stays compact by default and keeps db-exec / seed-* / extension /
1309
1406
  // browser-session footguns off the external surface.
1310
1407
  const connectorCatalogActive =
1311
- Array.isArray(config.connectorCatalog) &&
1312
- config.connectorCatalog.length > 0 &&
1408
+ (connectorNames.size > 0 || automaticConnectorPolicyActive) &&
1313
1409
  !fullCatalogRequested;
1314
1410
  // When the connector catalog is active, filter directly from visibleActions
1315
1411
  // rather than advertisedActionsBeforeConnector. This ensures the connector
@@ -1318,9 +1414,18 @@ export async function createMCPServerForRequest(
1318
1414
  // would have activated the compact catalog for the same caller.
1319
1415
  const advertisedActions = connectorCatalogActive
1320
1416
  ? Object.fromEntries(
1321
- Object.entries(visibleActions).filter(([name]) =>
1322
- isActionInConnectorCatalog(name, config),
1323
- ),
1417
+ Object.entries(visibleActions).filter(([name, entry]) => {
1418
+ if (denyNames.has(name)) return false;
1419
+ if (COMPACT_MCP_APP_CATALOG_BUILTINS.has(name)) return true;
1420
+ if (!connectorNames.has(name)) return false;
1421
+ if (
1422
+ externalAgentWritesAreAskAppOnly(config) &&
1423
+ entry.readOnly !== true
1424
+ ) {
1425
+ return false;
1426
+ }
1427
+ return true;
1428
+ }),
1324
1429
  )
1325
1430
  : advertisedActionsBeforeConnector;
1326
1431
  if (fullCatalogRequested) {
@@ -858,8 +858,11 @@ function askAppTool(
858
858
  return {
859
859
  tool: tool(
860
860
  "Send a natural-language message to an app's AI agent and get its " +
861
- "response. Use for complex, multi-step tasks needing the agent's " +
862
- "reasoning and full app context. In a single-app project the 'app' " +
861
+ "response. Use this first for natural-language investigation, " +
862
+ "diagnosis, multi-step work, and changes; it runs with the app's " +
863
+ "full skills, instructions, tools, and context. Use direct action " +
864
+ "tools only for a known, bounded read or simple UI handoff. In a " +
865
+ "single-app project the 'app' " +
863
866
  "param is optional (defaults to this app). When 'app' names a " +
864
867
  "different workspace app it is routed there over A2A; the result's " +
865
868
  "'routedVia' field reports whether it ran cross-app or locally. " +
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Policy for the default authenticated external-agent connector surface.
3
+ *
4
+ * The policy is intentionally narrow: "auto" only discovers actions that
5
+ * explicitly opt into authenticated, read-only public-agent exposure. It
6
+ * never makes an unannotated action callable and never grants writes.
7
+ */
8
+ export interface ExternalAgentPolicy {
9
+ /** Automatically advertise authenticated GET/read-only public-agent actions. */
10
+ authenticatedReads?: "off" | "auto";
11
+ /**
12
+ * Keep writes behind `ask_app` by default. `allowlisted` preserves explicit
13
+ * connectorCatalog write exposure for apps that intentionally need it.
14
+ */
15
+ writes?: "ask_app_only" | "allowlisted";
16
+ /** Explicit action names to remove from the derived connector surface. */
17
+ denyActions?: string[];
18
+ }
@@ -10,6 +10,7 @@ export {
10
10
  buildLinkArtifacts,
11
11
  } from "./build-server.js";
12
12
  export type { MCPCallerIdentity, MCPRequestMeta } from "./build-server.js";
13
+ export type { ExternalAgentPolicy } from "./external-agent-policy.js";
13
14
 
14
15
  // stdio transport for `agent-native mcp serve` (Node-only).
15
16
  export { runMCPStdio } from "./stdio.js";
@@ -5,6 +5,7 @@ import type {
5
5
  AgentChatReference,
6
6
  MentionProvider,
7
7
  } from "../../agent/types.js";
8
+ import type { ExternalAgentPolicy } from "../../mcp/external-agent-policy.js";
8
9
  import type { DatabaseToolsOption } from "../../scripts/db/tool-mode.js";
9
10
  import type { PromptExamples } from "../prompts/index.js";
10
11
 
@@ -293,7 +294,9 @@ export interface AgentChatPluginOptions {
293
294
  * browser-session tools) from connectors. It is no longer gated behind an
294
295
  * environment variable, and the catalog is never inferred from the client.
295
296
  *
296
- * `tool-search` stays available so any trimmed tool is reachable on demand.
297
+ * `tool-search` stays available for discovery; a trimmed action still needs
298
+ * the connector catalog, authenticated-read policy, or full-catalog opt-in
299
+ * before an external caller can execute it.
297
300
  * Callers who need the full surface up front opt in explicitly with
298
301
  * `agent-native connect --full-catalog` (embeds a `catalog_scope: "full"`
299
302
  * claim in their connect-minted JWT) or the deployment-wide
@@ -303,6 +306,14 @@ export interface AgentChatPluginOptions {
303
306
  */
304
307
  connectorCatalog?: string[];
305
308
 
309
+ /**
310
+ * Default authenticated external-agent policy. In `auto` read mode, every
311
+ * action explicitly marked as GET + readOnly + publicAgent.requiresAuth is
312
+ * added to the connector surface automatically. Writes remain ask_app-only
313
+ * unless `writes: "allowlisted"` is explicitly selected.
314
+ */
315
+ externalAgents?: ExternalAgentPolicy;
316
+
306
317
  /**
307
318
  * Skip mounting the remote MCP protocol route.
308
319
  *
@@ -31,7 +31,10 @@ import {
31
31
  function wrapCliScript(
32
32
  tool: ActionTool,
33
33
  cliDefault: (args: string[]) => Promise<void>,
34
- opts?: { readOnly?: boolean },
34
+ opts?: {
35
+ allowedArgs?: readonly string[];
36
+ readOnly?: boolean;
37
+ },
35
38
  ): ActionEntry {
36
39
  return {
37
40
  tool,
@@ -39,6 +42,14 @@ function wrapCliScript(
39
42
  run: async (args: Record<string, string>): Promise<string> => {
40
43
  const cliArgs: string[] = [];
41
44
  for (const [k, v] of Object.entries(args)) {
45
+ // MCP input schemas are descriptive and some hosts can still send
46
+ // undeclared keys. The externally exposed DB readers must never accept
47
+ // the CLI-only `--db` escape hatch, which could point at another local
48
+ // SQLite file. Keep their runtime surface identical to the advertised
49
+ // schema instead of trusting the client to validate it.
50
+ if (opts?.allowedArgs && !opts.allowedArgs.includes(k)) {
51
+ throw new Error(`Unknown argument: ${k}`);
52
+ }
42
53
  const raw = v as unknown;
43
54
  const value =
44
55
  raw != null && typeof raw === "object"
@@ -101,7 +112,10 @@ export async function createDbScriptEntries(
101
112
  },
102
113
  },
103
114
  schemaMod.default,
104
- { readOnly: true },
115
+ {
116
+ allowedArgs: ["format"],
117
+ readOnly: true,
118
+ },
105
119
  ),
106
120
  "db-query": wrapCliScript(
107
121
  {
@@ -134,7 +148,10 @@ export async function createDbScriptEntries(
134
148
  },
135
149
  },
136
150
  queryMod.default,
137
- { readOnly: true },
151
+ {
152
+ allowedArgs: ["sql", "args", "format", "limit"],
153
+ readOnly: true,
154
+ },
138
155
  ),
139
156
  };
140
157
 
@@ -1512,6 +1512,9 @@ export function createAgentChatPlugin(
1512
1512
  ...(options?.connectorCatalog
1513
1513
  ? { connectorCatalog: options.connectorCatalog }
1514
1514
  : {}),
1515
+ ...(options?.externalAgents
1516
+ ? { externalAgents: options.externalAgents }
1517
+ : {}),
1515
1518
  askAgent: async (message: string) => {
1516
1519
  const mcpEngine = await resolveEngine({
1517
1520
  engineOption: options?.engine,
@@ -160,15 +160,44 @@ tiny by default for ChatGPT/Claude-style app hosts, including OAuth MCP Apps
160
160
  callers and generic authenticated remote HTTP/static-token callers. The model
161
161
  sees the generic app-facing verbs (`list_apps`, `open_app`, `ask_app`, and
162
162
  app-only `create_embed_session`) and routes UI through
163
- `open_app({ embed: true })`. Stdio/code clients that explicitly identify as
164
- developer clients keep the full connected action surface, and
165
- `publicAgent.expose` remains the opt-in for safe read/ingest tools outside the
166
- compact MCP Apps catalog. Do not rely on action-specific `mcpApp` resources
167
- appearing in ChatGPT/Claude discovery by default; use `open_app` for the
163
+ `open_app({ embed: true })`. Stdio/code clients use the same compact surface
164
+ unless they explicitly opt into the full catalog, and
165
+ `publicAgent.expose` remains the action-level opt-in for safe read/ingest tools
166
+ outside the compact MCP Apps catalog. Apps can set
167
+ `externalAgents.authenticatedReads: "auto"` to automatically advertise every
168
+ action that is GET + `readOnly` + `publicAgent.requiresAuth`, instead of
169
+ maintaining a long connector allow-list. This policy never includes writes by
170
+ default; use `writes: "ask_app_only"` to keep mutations behind `ask_app`, and
171
+ use `denyActions` for defense-in-depth vetoes. `tool-search` remains available
172
+ for discovery, but a searched action still needs to be in the connector
173
+ catalog, included by the authenticated-read policy, or enabled by the explicit
174
+ full-catalog opt-in before `tools/call` can execute it. Do not rely on
175
+ action-specific `mcpApp` resources appearing in ChatGPT/Claude discovery by
176
+ default; use `open_app` for the
168
177
  first-class app embed path. If a specific
169
178
  action truly must remain visible in that compact app-host catalog, set
170
179
  `mcpApp.compactCatalog: true` as a rare escape hatch.
171
180
 
181
+ Generic core `db-schema` and `db-query` are intentionally not automatic
182
+ external reads. They remain available to the in-app agent through the normal
183
+ scoped SQL path, but broad schema/SQL access is too powerful to infer from
184
+ read-only metadata alone. If an app needs direct external querying, expose an
185
+ app-owned bounded GET action or add an explicit table/column allow-list with
186
+ row, byte, timeout, and audit limits. Keep `db-exec` and `db-patch` outside
187
+ automatic external-agent exposure.
188
+
189
+ Authentication and authorization are separate gates. A verified MCP OAuth or
190
+ connect token identifies the caller and organization; `publicAgent` only opts
191
+ an action into the external protocol surface and does not grant record access.
192
+ Actions still need `accessFilter`, `resolveAccess`, or `assertAccess` so private
193
+ documents/dashboards, shares, organization boundaries, and roles are enforced.
194
+ For Slack, verified DMs are linked to an existing Agent Native org member
195
+ before execution and run with that user's context; shared channels use a
196
+ service principal, and guests/external members cannot borrow personal access.
197
+ Managed Slack OAuth and the generated app manifest both request
198
+ `users:read.email`; existing installs must reconnect/reinstall after a scope
199
+ change, while legacy bot-token installs must add the scope in Slack manually.
200
+
172
201
  ### 1b. Fast-path expectations for MCP Apps hosts
173
202
 
174
203
  Keep ChatGPT/Claude paths short. For a known app-facing intent, the external
@@ -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.
@@ -113,11 +113,26 @@ details live in `.agents/skills/`.
113
113
  clicks when needed. The authenticated connector catalog is the fast fallback
114
114
  and exposes only bounded, user/org-scoped read actions for incident triage:
115
115
  `list-session-recordings`, `get-session-replay-summary`,
116
- `get-session-replay-events`, `query-agent-native-analytics`,
117
- `list-error-issues`, and `get-error-issue`. Fetch the summary before raw
118
- events, use the event filters/limits to keep the response bounded, and do not
119
- add replay blob or dashboard mutation actions to this catalog without an
120
- explicit security review.
116
+ `get-session-replay-timeline`, `query-agent-native-analytics`,
117
+ `list-error-issues`, and `get-error-issue`. Fetch the summary before the
118
+ sanitized timeline; the timeline contains bounded page/click/error markers
119
+ without raw replay events or storage references. Do not add replay blob or
120
+ dashboard mutation actions to this catalog without an explicit security
121
+ review.
122
+ - While Demo mode is enabled, session and error identities display anonymized
123
+ (`anonymous@builder.io`), so incident lookups must filter by the stable
124
+ `userId`/email parameter (e.g. `userId: "user@example.com"`) or recording/
125
+ issue id — never by matching emails read back from previously displayed
126
+ output.
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.
121
136
  - `/agents` is the Analytics home for admin surfaces. The default Monitoring
122
137
  view embeds the shared observability dashboard for traces, conversations,
123
138
  evals, experiments, and feedback. `/agents?view=dashboards` shows the
@@ -20,7 +20,6 @@ export default defineAction({
20
20
  schema: z.object({
21
21
  recordingId: z.string().describe("The session_recordings id"),
22
22
  }),
23
- readOnly: true,
24
23
  run: async (args) => {
25
24
  return createSessionReplayAgentLink({
26
25
  recordingId: args.recordingId,
@@ -40,7 +40,6 @@ export default defineAction({
40
40
  }),
41
41
  http: { method: "GET" },
42
42
  readOnly: true,
43
- publicAgent: { expose: true, readOnly: true, requiresAuth: true },
44
43
  run: async (args) => {
45
44
  return getSessionReplayEvents(args.recordingId, resolveScope(), {
46
45
  startSeq: args.startSeq,
@@ -0,0 +1,40 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import {
3
+ getRequestOrgId,
4
+ getRequestUserEmail,
5
+ } from "@agent-native/core/server";
6
+ import { z } from "zod";
7
+
8
+ import { getSessionReplayTimeline } from "../server/lib/session-replay-agent-context.js";
9
+
10
+ function resolveScope() {
11
+ const userEmail = getRequestUserEmail();
12
+ if (!userEmail) throw new Error("no authenticated user");
13
+ return { userEmail, orgId: getRequestOrgId() || null };
14
+ }
15
+
16
+ export default defineAction({
17
+ description:
18
+ "Get a bounded, sanitized timeline for one first-party Analytics session recording: page navigation, clicks, inputs, scrolls, custom events, console errors, and failed network requests. Returns marker metadata only; never returns raw rrweb/DOM/input values, replay chunks, or storage references. Use this after get-session-replay-summary when direct incident triage needs the user's click/page sequence.",
19
+ schema: z.object({
20
+ recordingId: z.string().describe("The session_recordings id"),
21
+ eventLimit: z.coerce
22
+ .number()
23
+ .int()
24
+ .min(1)
25
+ .max(10000)
26
+ .optional()
27
+ .default(10000)
28
+ .describe(
29
+ "Maximum replay events to inspect before building the capped timeline",
30
+ ),
31
+ }),
32
+ http: { method: "GET" },
33
+ readOnly: true,
34
+ publicAgent: { expose: true, readOnly: true, requiresAuth: true },
35
+ run: async (args) => {
36
+ return getSessionReplayTimeline(args.recordingId, resolveScope(), {
37
+ eventLimit: args.eventLimit,
38
+ });
39
+ },
40
+ });
@@ -26,6 +26,18 @@ export default defineAction({
26
26
  .optional()
27
27
  .describe("Search across issue title, type, culprit, and fingerprint."),
28
28
  app: z.string().optional().describe("Optional app filter."),
29
+ sessionRecordingId: z
30
+ .string()
31
+ .optional()
32
+ .describe(
33
+ "Filter to issues with an occurrence in this session recording.",
34
+ ),
35
+ userId: z
36
+ .string()
37
+ .optional()
38
+ .describe(
39
+ "Filter to issues with an occurrence for this user ID or user key.",
40
+ ),
29
41
  sort: z
30
42
  .enum(["lastSeen", "eventCount", "firstSeen"])
31
43
  .optional()
@@ -33,7 +33,10 @@ export default defineAction({
33
33
  ),
34
34
  }),
35
35
  readOnly: true,
36
- http: { method: "GET" },
36
+ // No raw HTTP route: a GET endpoint would put caller SQL in query strings
37
+ // and access logs. External MCP callers reach this through the explicit
38
+ // ANALYTICS_CONNECTOR_CATALOG entry, which dispatches run() in-process.
39
+ http: false,
37
40
  publicAgent: { expose: true, readOnly: true, requiresAuth: true },
38
41
  run: async (args) => {
39
42
  return queryFirstPartyAnalytics(args.sql, resolveScope());
@@ -21,7 +21,7 @@ import {
21
21
  IconSettings,
22
22
  } from "@tabler/icons-react";
23
23
  import { useQuery } from "@tanstack/react-query";
24
- import { useCallback, useEffect, useMemo, useState } from "react";
24
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
25
25
  import { Link, useNavigate, useSearchParams } from "react-router";
26
26
  import { toast } from "sonner";
27
27
 
@@ -169,6 +169,40 @@ type SessionRecordingDevice = Pick<SessionRecordingSummary, "metadata">;
169
169
  const RANGE_OPTIONS: ReplayRange[] = ["24h", "7d", "30d", "90d", "all"];
170
170
  const SESSION_QUERY_DEBOUNCE_MS = 250;
171
171
 
172
+ /**
173
+ * Local input state for a URL-backed filter, debounced into the URL.
174
+ *
175
+ * `urlValue` only resyncs local state when it changes for a reason other
176
+ * than this hook's own debounced write (back/forward navigation, an agent
177
+ * driven URL change, etc). React Router commits `setSearchParams` inside a
178
+ * transition, so without this guard the echo of our own write can land
179
+ * after a newer keystroke and clobber it.
180
+ */
181
+ export function useDebouncedUrlFilter(
182
+ urlValue: string,
183
+ onCommit: (value: string) => void,
184
+ ): [string, (value: string) => void] {
185
+ const [input, setInput] = useState(urlValue);
186
+ const lastPushedRef = useRef(urlValue);
187
+
188
+ useEffect(() => {
189
+ if (urlValue === lastPushedRef.current) return;
190
+ lastPushedRef.current = urlValue;
191
+ setInput(urlValue);
192
+ }, [urlValue]);
193
+
194
+ useEffect(() => {
195
+ if (input === urlValue) return;
196
+ const timeout = window.setTimeout(() => {
197
+ lastPushedRef.current = input;
198
+ onCommit(input);
199
+ }, SESSION_QUERY_DEBOUNCE_MS);
200
+ return () => window.clearTimeout(timeout);
201
+ }, [input, urlValue, onCommit]);
202
+
203
+ return [input, setInput];
204
+ }
205
+
172
206
  export default function SessionsPage() {
173
207
  const t = useT();
174
208
  const navigate = useNavigate();
@@ -176,13 +210,8 @@ export default function SessionsPage() {
176
210
  const range = readRange(searchParams.get("range"));
177
211
  const app = searchParams.get("app") ?? "";
178
212
  const query = searchParams.get("q") ?? "";
179
- const [queryInput, setQueryInput] = useState(query);
180
213
  const from = useMemo(() => rangeToFrom(range), [range]);
181
214
 
182
- useEffect(() => {
183
- setQueryInput(query);
184
- }, [query]);
185
-
186
215
  const updateFilter = useCallback(
187
216
  (key: string, value: string) => {
188
217
  setSearchParams(
@@ -200,13 +229,17 @@ export default function SessionsPage() {
200
229
  [setSearchParams],
201
230
  );
202
231
 
203
- useEffect(() => {
204
- if (queryInput === query) return;
205
- const timeout = window.setTimeout(() => {
206
- updateFilter("q", queryInput);
207
- }, SESSION_QUERY_DEBOUNCE_MS);
208
- return () => window.clearTimeout(timeout);
209
- }, [query, queryInput, updateFilter]);
232
+ const commitQuery = useCallback(
233
+ (value: string) => updateFilter("q", value),
234
+ [updateFilter],
235
+ );
236
+ const [queryInput, setQueryInput] = useDebouncedUrlFilter(query, commitQuery);
237
+
238
+ const commitApp = useCallback(
239
+ (value: string) => updateFilter("app", value),
240
+ [updateFilter],
241
+ );
242
+ const [appInput, setAppInput] = useDebouncedUrlFilter(app, commitApp);
210
243
 
211
244
  const { data, isLoading, isFetching, refetch, error } = useActionQuery<
212
245
  SessionRecordingSummary[]
@@ -291,10 +324,8 @@ export default function SessionsPage() {
291
324
  {t("sessions.userFilters")}
292
325
  </div>
293
326
  <Input
294
- value={app}
295
- onChange={(event) =>
296
- updateFilter("app", event.target.value)
297
- }
327
+ value={appInput}
328
+ onChange={(event) => setAppInput(event.target.value)}
298
329
  placeholder={t("sessions.appPlaceholder")}
299
330
  className="h-9"
300
331
  />
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Analytics uses the full in-app agent for multi-step incident investigations, with direct read tools available for focused lookups.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Connected external agents can now look up sessions, error issues, and sanitized replay timelines directly, with the app agent remaining the default path.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-12
4
+ ---
5
+
6
+ Session identities stay visible when Demo mode is off