@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
|
@@ -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
|
|
128
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
862
|
-
"
|
|
861
|
+
"response. Use this first for natural-language investigation, " +
|
|
862
|
+
"diagnosis, multi-step work, and changes; it runs with the app's " +
|
|
863
|
+
"full skills, instructions, tools, and context. Use direct action " +
|
|
864
|
+
"tools only for a known, bounded read or simple UI handoff. In a " +
|
|
865
|
+
"single-app project the 'app' " +
|
|
863
866
|
"param is optional (defaults to this app). When 'app' names a " +
|
|
864
867
|
"different workspace app it is routed there over A2A; the result's " +
|
|
865
868
|
"'routedVia' field reports whether it ran cross-app or locally. " +
|
|
@@ -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
|
|
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?: {
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
164
|
-
|
|
165
|
-
`publicAgent.expose` remains the opt-in for safe read/ingest tools
|
|
166
|
-
compact MCP Apps catalog.
|
|
167
|
-
|
|
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-
|
|
117
|
-
`list-error-issues`, and `get-error-issue`. Fetch the summary before
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
explicit security
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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={
|
|
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
|
/>
|