@agent-native/core 0.125.0 → 0.127.1
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 +79 -0
- package/corpus/core/docs/content/external-agents-catalog.mdx +14 -11
- package/corpus/core/docs/content/mcp-protocol.mdx +9 -4
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/index.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +6 -1
- package/corpus/core/src/agent/run-manager.ts +37 -0
- package/corpus/core/src/agent/run-store.ts +18 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +75 -43
- package/corpus/core/src/client/agent-chat.ts +19 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +25 -0
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +23 -0
- package/corpus/core/src/client/settings/AgentsSection.tsx +565 -71
- package/corpus/core/src/client/settings/SecretsSection.tsx +71 -36
- package/corpus/core/src/client/use-chat-models.ts +54 -37
- package/corpus/core/src/localization/default-messages.ts +11 -0
- package/corpus/core/src/mcp/ask-app-task-handle.ts +183 -0
- package/corpus/core/src/mcp/builtin-tools.ts +146 -34
- package/corpus/core/src/server/agent-capabilities.ts +8 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +17 -0
- package/corpus/core/src/server/agent-discovery.ts +1 -1
- package/corpus/core/src/server/agent-peer-probe.ts +133 -0
- package/corpus/core/src/server/core-routes-plugin.ts +67 -0
- package/corpus/core/src/server/email-template.ts +13 -0
- package/corpus/core/src/server/email.ts +40 -4
- package/corpus/core/src/shared/mcp-embed-headers.ts +4 -0
- package/corpus/core/src/sharing/actions/share-resource.ts +55 -5
- package/corpus/core/src/sharing/registry.ts +41 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/corpus/templates/assets/actions/edit-image.ts +31 -18
- package/corpus/templates/assets/actions/refine-image.ts +43 -29
- package/corpus/templates/assets/actions/restyle-image.ts +30 -19
- package/corpus/templates/assets/actions/variant-slots.ts +26 -0
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
- package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
- package/corpus/templates/clips/server/db/index.ts +28 -6
- package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
- package/corpus/templates/design/app/hooks/use-question-flow.ts +21 -1
- package/corpus/templates/design/changelog/2026-07-28-the-model-you-pick-when-starting-a-new-design-is-now-used-fo.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-28-pending-apps-are-hidden-by-default-and-app-ownership-details.md +6 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +5 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +31 -8
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +15 -0
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +78 -8
- package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +179 -0
- package/corpus/templates/slides/app/components/presentation/present-channel.ts +15 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +10 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +67 -0
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +16 -3
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +11 -0
- package/corpus/templates/slides/app/pages/Presentation.tsx +11 -3
- package/corpus/templates/slides/changelog/2026-07-28-export-to-google-slides-now-creates-the-deck-directly-in-you.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-28-opening-a-presentation-link-directly-no-longer-bounces-back-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-28-presenter-view-open-a-second-window-while-presenting-to-see-.md +6 -0
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +90 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/composer/TiptapComposer.tsx +143 -111
- package/corpus/toolkit/src/composer/runtime-adapters.tsx +2 -0
- package/dist/a2a/index.d.ts +2 -0
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -0
- package/dist/a2a/index.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +6 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +11 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +34 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +8 -0
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +16 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +60 -31
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +6 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +13 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +14 -0
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts +6 -0
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +16 -0
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/settings/AgentsSection.d.ts.map +1 -1
- package/dist/client/settings/AgentsSection.js +250 -30
- package/dist/client/settings/AgentsSection.js.map +1 -1
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +16 -2
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +46 -27
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +9 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +9 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/ask-app-task-handle.d.ts +25 -0
- package/dist/mcp/ask-app-task-handle.d.ts.map +1 -0
- package/dist/mcp/ask-app-task-handle.js +136 -0
- package/dist/mcp/ask-app-task-handle.js.map +1 -0
- package/dist/mcp/builtin-tools.d.ts +3 -3
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +115 -38
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/provider-api/actions/provider-api.d.ts +6 -6
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-capabilities.d.ts +7 -1
- package/dist/server/agent-capabilities.d.ts.map +1 -1
- package/dist/server/agent-capabilities.js +1 -0
- package/dist/server/agent-capabilities.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +7 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +16 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-discovery.d.ts +1 -0
- package/dist/server/agent-discovery.d.ts.map +1 -1
- package/dist/server/agent-discovery.js +1 -1
- package/dist/server/agent-discovery.js.map +1 -1
- package/dist/server/agent-peer-probe.d.ts +37 -0
- package/dist/server/agent-peer-probe.d.ts.map +1 -0
- package/dist/server/agent-peer-probe.js +90 -0
- package/dist/server/agent-peer-probe.js.map +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +53 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/email-template.d.ts +6 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +6 -0
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/email.d.ts +7 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +31 -6
- package/dist/server/email.js.map +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +1 -0
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +4 -0
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/sharing/actions/share-resource.d.ts.map +1 -1
- package/dist/sharing/actions/share-resource.js +49 -5
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/registry.d.ts +40 -1
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
- package/docs/content/external-agents-catalog.mdx +14 -11
- package/docs/content/mcp-protocol.mdx +9 -4
- package/package.json +2 -2
- package/src/a2a/index.ts +2 -0
- package/src/agent/production-agent.ts +6 -1
- package/src/agent/run-manager.ts +37 -0
- package/src/agent/run-store.ts +18 -0
- package/src/client/MultiTabAssistantChat.tsx +75 -43
- package/src/client/agent-chat.ts +19 -1
- package/src/client/chat/tool-call-display.tsx +25 -0
- package/src/client/resources/mcp-integration-catalog.ts +23 -0
- package/src/client/settings/AgentsSection.tsx +565 -71
- package/src/client/settings/SecretsSection.tsx +71 -36
- package/src/client/use-chat-models.ts +54 -37
- package/src/localization/default-messages.ts +11 -0
- package/src/mcp/ask-app-task-handle.ts +183 -0
- package/src/mcp/builtin-tools.ts +146 -34
- package/src/server/agent-capabilities.ts +8 -1
- package/src/server/agent-chat-plugin.ts +17 -0
- package/src/server/agent-discovery.ts +1 -1
- package/src/server/agent-peer-probe.ts +133 -0
- package/src/server/core-routes-plugin.ts +67 -0
- package/src/server/email-template.ts +13 -0
- package/src/server/email.ts +40 -4
- package/src/shared/mcp-embed-headers.ts +4 -0
- package/src/sharing/actions/share-resource.ts +55 -5
- package/src/sharing/registry.ts +41 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +159 -119
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* `open_app` / `create_workspace_app` return an **absolute** URL on the
|
|
22
22
|
* *target* app's origin when it differs from this app (so a workspace link
|
|
23
23
|
* lands in the right app), and a relative path for the same app / standalone.
|
|
24
|
-
* `ask_app` routes
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* `ask_app` routes a known *different* workspace app over A2A and reports
|
|
25
|
+
* `routedVia: "a2a"`; unknown cross-app targets fail closed instead of running
|
|
26
|
+
* on the current app. Same-app requests report `routedVia: "local"`.
|
|
27
27
|
* | `list_templates` | none | `{ templates: [...] }` (allow-list only) |
|
|
28
28
|
*
|
|
29
29
|
* Node-only at call time (workspace resolution + scaffolding use `fs`), but
|
|
@@ -36,7 +36,15 @@ import type { ActionEntry } from "../agent/production-agent.js";
|
|
|
36
36
|
import type { ActionTool } from "../agent/types.js";
|
|
37
37
|
import { getConfiguredAppBasePath } from "../server/app-base-path.js";
|
|
38
38
|
import { buildDeepLink } from "../server/deep-link.js";
|
|
39
|
+
import {
|
|
40
|
+
getRequestOrgId,
|
|
41
|
+
getRequestUserEmail,
|
|
42
|
+
} from "../server/request-context.js";
|
|
39
43
|
import { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from "../shared/embed-auth.js";
|
|
44
|
+
import {
|
|
45
|
+
signAskAppTaskHandle,
|
|
46
|
+
verifyAskAppTaskHandle,
|
|
47
|
+
} from "./ask-app-task-handle.js";
|
|
40
48
|
import type { MCPConfig } from "./build-server.js";
|
|
41
49
|
import { embedApp } from "./embed-app.js";
|
|
42
50
|
import { fetchOrgApps, type OrgApp } from "./org-directory.js";
|
|
@@ -112,6 +120,7 @@ interface AskAppTaskResult {
|
|
|
112
120
|
app: string;
|
|
113
121
|
routedVia: "local" | "a2a";
|
|
114
122
|
taskId: string;
|
|
123
|
+
taskHandle?: string;
|
|
115
124
|
status: string;
|
|
116
125
|
response?: string;
|
|
117
126
|
error?: string;
|
|
@@ -120,7 +129,7 @@ interface AskAppTaskResult {
|
|
|
120
129
|
pollAfterMs?: number;
|
|
121
130
|
poll?: {
|
|
122
131
|
tool: "ask_app_status";
|
|
123
|
-
arguments: { app: string; taskId: string };
|
|
132
|
+
arguments: { app: string; taskId: string; taskHandle?: string };
|
|
124
133
|
};
|
|
125
134
|
message?: string;
|
|
126
135
|
statusRead?: "unavailable";
|
|
@@ -200,6 +209,27 @@ function selfA2AEndpointUrl(requestMeta?: AskAppRequestMeta): string | null {
|
|
|
200
209
|
return agentNativeA2AEndpoint(`${origin}${basePath}`);
|
|
201
210
|
}
|
|
202
211
|
|
|
212
|
+
function askAppIssuerAudience(requestMeta?: AskAppRequestMeta): string | null {
|
|
213
|
+
const origin = requestMeta?.origin?.replace(/\/+$/, "");
|
|
214
|
+
if (!origin) return null;
|
|
215
|
+
try {
|
|
216
|
+
const originUrl = new URL(origin);
|
|
217
|
+
if (
|
|
218
|
+
!["http:", "https:"].includes(originUrl.protocol) ||
|
|
219
|
+
originUrl.username ||
|
|
220
|
+
originUrl.password
|
|
221
|
+
) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const basePath = requestMeta?.basePath || getConfiguredAppBasePath();
|
|
225
|
+
const audience = new URL(basePath || "/", `${originUrl.origin}/`);
|
|
226
|
+
const pathname = audience.pathname.replace(/\/+$/, "");
|
|
227
|
+
return `${audience.origin}${pathname}`;
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
203
233
|
function boundedAskAppWaitMs(raw: unknown): number {
|
|
204
234
|
if (raw == null || raw === "") return ASK_APP_DEFAULT_INLINE_WAIT_MS;
|
|
205
235
|
const parsed = Number(raw);
|
|
@@ -231,13 +261,18 @@ function taskText(task: Task): string {
|
|
|
231
261
|
);
|
|
232
262
|
}
|
|
233
263
|
|
|
234
|
-
function askAppTaskResult(
|
|
264
|
+
function askAppTaskResult(
|
|
265
|
+
route: AskAppRoute,
|
|
266
|
+
task: Task,
|
|
267
|
+
taskHandle?: string,
|
|
268
|
+
): AskAppTaskResult {
|
|
235
269
|
const status = taskState(task);
|
|
236
270
|
const response = taskText(task);
|
|
237
271
|
const base = {
|
|
238
272
|
app: route.app,
|
|
239
273
|
routedVia: route.routedVia,
|
|
240
274
|
taskId: task.id,
|
|
275
|
+
...(taskHandle ? { taskHandle } : {}),
|
|
241
276
|
status,
|
|
242
277
|
...(route.note ? { note: route.note } : {}),
|
|
243
278
|
};
|
|
@@ -273,11 +308,17 @@ function askAppTaskResult(route: AskAppRoute, task: Task): AskAppTaskResult {
|
|
|
273
308
|
pollAfterMs: ASK_APP_POLL_INTERVAL_MS,
|
|
274
309
|
poll: {
|
|
275
310
|
tool: "ask_app_status",
|
|
276
|
-
arguments: {
|
|
311
|
+
arguments: {
|
|
312
|
+
app: route.app,
|
|
313
|
+
taskId: task.id,
|
|
314
|
+
...(taskHandle ? { taskHandle } : {}),
|
|
315
|
+
},
|
|
277
316
|
},
|
|
278
317
|
message:
|
|
279
318
|
`ask_app is still ${status}. Call ask_app_status with ` +
|
|
280
|
-
|
|
319
|
+
(taskHandle
|
|
320
|
+
? "the returned taskHandle to retrieve the final response."
|
|
321
|
+
: `taskId "${task.id}" to retrieve the final response.`),
|
|
281
322
|
};
|
|
282
323
|
}
|
|
283
324
|
|
|
@@ -412,6 +453,8 @@ async function waitForA2ATask(
|
|
|
412
453
|
|
|
413
454
|
async function submitAskAppA2ATask(
|
|
414
455
|
route: AskAppRoute,
|
|
456
|
+
issuerApp: string,
|
|
457
|
+
issuerAudience: string,
|
|
415
458
|
message: string,
|
|
416
459
|
maxWaitMs: number,
|
|
417
460
|
approvedActions?: A2AApprovedAction[],
|
|
@@ -434,12 +477,23 @@ async function submitAskAppA2ATask(
|
|
|
434
477
|
},
|
|
435
478
|
);
|
|
436
479
|
const finalOrRunning = await waitForA2ATask(client, task, deadline);
|
|
437
|
-
|
|
480
|
+
const subject =
|
|
481
|
+
typeof metadata.userEmail === "string" ? metadata.userEmail : "anonymous";
|
|
482
|
+
const taskHandle = await signAskAppTaskHandle({
|
|
483
|
+
issuerApp,
|
|
484
|
+
issuerAudience,
|
|
485
|
+
organization: getRequestOrgId() ?? "",
|
|
486
|
+
subject,
|
|
487
|
+
route,
|
|
488
|
+
taskId: task.id,
|
|
489
|
+
});
|
|
490
|
+
return askAppTaskResult(route, finalOrRunning, taskHandle);
|
|
438
491
|
}
|
|
439
492
|
|
|
440
493
|
async function fetchAskAppA2ATask(
|
|
441
494
|
route: AskAppRoute,
|
|
442
495
|
taskId: string,
|
|
496
|
+
taskHandle?: string,
|
|
443
497
|
): Promise<AskAppTaskResult> {
|
|
444
498
|
const { client } = await createA2AClientForAskApp(route.origin);
|
|
445
499
|
const maxAttempts = ASK_APP_STATUS_RETRY_DELAYS_MS.length + 1;
|
|
@@ -451,7 +505,7 @@ async function fetchAskAppA2ATask(
|
|
|
451
505
|
const startedAt = Date.now();
|
|
452
506
|
try {
|
|
453
507
|
const task = await client.getTask(taskId);
|
|
454
|
-
return askAppTaskResult(route, task);
|
|
508
|
+
return askAppTaskResult(route, task, taskHandle);
|
|
455
509
|
} catch (err) {
|
|
456
510
|
const delayMs = ASK_APP_STATUS_RETRY_DELAYS_MS[attempt];
|
|
457
511
|
const errorCategory = askAppStatusErrorCategory(err);
|
|
@@ -482,6 +536,7 @@ async function fetchAskAppA2ATask(
|
|
|
482
536
|
taskId,
|
|
483
537
|
errorCategory,
|
|
484
538
|
maxAttempts,
|
|
539
|
+
taskHandle,
|
|
485
540
|
);
|
|
486
541
|
}
|
|
487
542
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -541,11 +596,13 @@ function askAppStatusReadUnavailableResult(
|
|
|
541
596
|
taskId: string,
|
|
542
597
|
errorCategory: AskAppStatusErrorCategory,
|
|
543
598
|
attempts: number,
|
|
599
|
+
taskHandle?: string,
|
|
544
600
|
): AskAppTaskResult {
|
|
545
601
|
return {
|
|
546
602
|
app: route.app,
|
|
547
603
|
routedVia: route.routedVia,
|
|
548
604
|
taskId,
|
|
605
|
+
...(taskHandle ? { taskHandle } : {}),
|
|
549
606
|
status: "unknown",
|
|
550
607
|
statusRead: "unavailable",
|
|
551
608
|
retryable: true,
|
|
@@ -554,12 +611,18 @@ function askAppStatusReadUnavailableResult(
|
|
|
554
611
|
pollAfterMs: ASK_APP_POLL_INTERVAL_MS,
|
|
555
612
|
poll: {
|
|
556
613
|
tool: "ask_app_status",
|
|
557
|
-
arguments: {
|
|
614
|
+
arguments: {
|
|
615
|
+
app: route.app,
|
|
616
|
+
taskId,
|
|
617
|
+
...(taskHandle ? { taskHandle } : {}),
|
|
618
|
+
},
|
|
558
619
|
},
|
|
559
620
|
message:
|
|
560
621
|
"The durable ask_app task status could not be read after bounded retries. " +
|
|
561
622
|
"The task may still be running or completed. Retry ask_app_status " +
|
|
562
|
-
|
|
623
|
+
(taskHandle
|
|
624
|
+
? "with the same taskHandle; do not resubmit ask_app."
|
|
625
|
+
: "with the same app and taskId; do not resubmit ask_app."),
|
|
563
626
|
};
|
|
564
627
|
}
|
|
565
628
|
|
|
@@ -631,7 +694,7 @@ function listAppsTool(
|
|
|
631
694
|
livePort = 0;
|
|
632
695
|
}
|
|
633
696
|
}
|
|
634
|
-
const selfId = (config
|
|
697
|
+
const selfId = currentAppId(config);
|
|
635
698
|
const isSelf = (id: string) =>
|
|
636
699
|
!!liveOrigin &&
|
|
637
700
|
(!ws.isWorkspace || (!!selfId && id.toLowerCase() === selfId));
|
|
@@ -644,7 +707,10 @@ function listAppsTool(
|
|
|
644
707
|
source: "workspace" | "org-directory";
|
|
645
708
|
}
|
|
646
709
|
|
|
647
|
-
const
|
|
710
|
+
const resolvedApps = ws.isWorkspace
|
|
711
|
+
? ws.apps
|
|
712
|
+
: ws.apps.map((app) => ({ ...app, id: selfId, name: config.name }));
|
|
713
|
+
const apps: AppEntry[] = resolvedApps.map((a) =>
|
|
648
714
|
isSelf(a.id)
|
|
649
715
|
? {
|
|
650
716
|
id: a.id,
|
|
@@ -991,6 +1057,8 @@ async function routeAskOverA2A(
|
|
|
991
1057
|
message: string,
|
|
992
1058
|
options?: {
|
|
993
1059
|
durable?: boolean;
|
|
1060
|
+
issuerApp?: string;
|
|
1061
|
+
issuerAudience?: string;
|
|
994
1062
|
maxWaitMs?: number;
|
|
995
1063
|
requestOrigin?: string;
|
|
996
1064
|
approvedActions?: A2AApprovedAction[];
|
|
@@ -999,6 +1067,11 @@ async function routeAskOverA2A(
|
|
|
999
1067
|
{ app: string; routedVia: "a2a"; response: string } | AskAppTaskResult
|
|
1000
1068
|
> {
|
|
1001
1069
|
if (options?.durable) {
|
|
1070
|
+
if (!options.issuerApp || !options.issuerAudience) {
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
"ask_app durable routing requires an issuer app id and audience.",
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1002
1075
|
return submitAskAppA2ATask(
|
|
1003
1076
|
{
|
|
1004
1077
|
app: id,
|
|
@@ -1006,6 +1079,8 @@ async function routeAskOverA2A(
|
|
|
1006
1079
|
routedVia: "a2a",
|
|
1007
1080
|
requestOrigin: options.requestOrigin ?? origin,
|
|
1008
1081
|
},
|
|
1082
|
+
options.issuerApp,
|
|
1083
|
+
options.issuerAudience,
|
|
1009
1084
|
message,
|
|
1010
1085
|
options.maxWaitMs ?? ASK_APP_DEFAULT_INLINE_WAIT_MS,
|
|
1011
1086
|
options.approvedActions,
|
|
@@ -1097,8 +1172,8 @@ function askAppTool(
|
|
|
1097
1172
|
"param is optional (defaults to this app). When 'app' names a " +
|
|
1098
1173
|
"different workspace app it is routed there over A2A; the result's " +
|
|
1099
1174
|
"'routedVia' field reports whether it ran cross-app or locally. " +
|
|
1100
|
-
"On hosted MCP, long tasks may return a durable
|
|
1101
|
-
"final response;
|
|
1175
|
+
"On hosted MCP, long tasks may return a durable taskHandle instead of " +
|
|
1176
|
+
"a final response; pass that handle to ask_app_status until completed.",
|
|
1102
1177
|
{
|
|
1103
1178
|
app: {
|
|
1104
1179
|
type: "string",
|
|
@@ -1111,12 +1186,12 @@ function askAppTool(
|
|
|
1111
1186
|
async: {
|
|
1112
1187
|
type: "boolean",
|
|
1113
1188
|
description:
|
|
1114
|
-
"When true, start a durable task and return immediately with a taskId.",
|
|
1189
|
+
"When true, start a durable task and return immediately with a taskHandle and legacy taskId.",
|
|
1115
1190
|
},
|
|
1116
1191
|
maxWaitMs: {
|
|
1117
1192
|
type: "number",
|
|
1118
1193
|
description:
|
|
1119
|
-
"Maximum time to wait inline before returning a
|
|
1194
|
+
"Maximum time to wait inline before returning a taskHandle. Hosted MCP clamps this to 25000ms.",
|
|
1120
1195
|
},
|
|
1121
1196
|
approvedActions: {
|
|
1122
1197
|
type: "array",
|
|
@@ -1140,6 +1215,12 @@ function askAppTool(
|
|
|
1140
1215
|
const requestedApp = String(args.app ?? "").trim();
|
|
1141
1216
|
const selfId = currentAppId(config);
|
|
1142
1217
|
const useDurableA2A = Boolean(requestMeta?.origin);
|
|
1218
|
+
const issuerAudience = askAppIssuerAudience(requestMeta);
|
|
1219
|
+
if (useDurableA2A && !issuerAudience) {
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
"ask_app durable routing requires a valid HTTP(S) issuer audience.",
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1143
1224
|
const maxWaitMs = isExplicitAsyncAsk(args.async)
|
|
1144
1225
|
? 0
|
|
1145
1226
|
: boundedAskAppWaitMs(args.maxWaitMs);
|
|
@@ -1160,6 +1241,8 @@ function askAppTool(
|
|
|
1160
1241
|
message,
|
|
1161
1242
|
{
|
|
1162
1243
|
durable: useDurableA2A,
|
|
1244
|
+
issuerApp: selfId,
|
|
1245
|
+
issuerAudience: issuerAudience ?? undefined,
|
|
1163
1246
|
maxWaitMs,
|
|
1164
1247
|
requestOrigin: targetApp.origin,
|
|
1165
1248
|
approvedActions,
|
|
@@ -1195,6 +1278,8 @@ function askAppTool(
|
|
|
1195
1278
|
message,
|
|
1196
1279
|
{
|
|
1197
1280
|
durable: useDurableA2A,
|
|
1281
|
+
issuerApp: selfId,
|
|
1282
|
+
issuerAudience: issuerAudience ?? undefined,
|
|
1198
1283
|
maxWaitMs,
|
|
1199
1284
|
requestOrigin: dirMatch.url,
|
|
1200
1285
|
approvedActions,
|
|
@@ -1209,7 +1294,14 @@ function askAppTool(
|
|
|
1209
1294
|
}
|
|
1210
1295
|
}
|
|
1211
1296
|
|
|
1212
|
-
|
|
1297
|
+
if (requestedApp && requestedApp.toLowerCase() !== selfId) {
|
|
1298
|
+
throw new Error(
|
|
1299
|
+
`No reachable ask_app route for app "${requestedApp}". ` +
|
|
1300
|
+
"Call list_apps and retry with an available app id.",
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Same app (or no target): answer locally with this
|
|
1213
1305
|
// app's own ask-agent handler — the same entry point the HTTP MCP mount
|
|
1214
1306
|
// + A2A use, so there is no second agent runner.
|
|
1215
1307
|
if (!config.askAgent) {
|
|
@@ -1218,15 +1310,6 @@ function askAppTool(
|
|
|
1218
1310
|
);
|
|
1219
1311
|
}
|
|
1220
1312
|
|
|
1221
|
-
// If the caller named an app we couldn't route to (unknown id, or no
|
|
1222
|
-
// workspace), say so honestly instead of claiming we reached it.
|
|
1223
|
-
const unresolved =
|
|
1224
|
-
!!requestedApp && requestedApp.toLowerCase() !== selfId;
|
|
1225
|
-
const note = unresolved
|
|
1226
|
-
? `Requested app "${requestedApp}" is not a reachable workspace ` +
|
|
1227
|
-
`app; answered with this app ("${selfId}") instead.`
|
|
1228
|
-
: undefined;
|
|
1229
|
-
|
|
1230
1313
|
// Hosted MCP cannot safely keep a JSON request/response open for a full
|
|
1231
1314
|
// agent loop: serverless gateways can return an inactivity 504 before
|
|
1232
1315
|
// the result body exists. When we know the running app origin, submit the
|
|
@@ -1240,8 +1323,9 @@ function askAppTool(
|
|
|
1240
1323
|
origin: localA2AEndpointUrl,
|
|
1241
1324
|
routedVia: "local",
|
|
1242
1325
|
requestOrigin: requestMeta?.origin,
|
|
1243
|
-
...(note ? { note } : {}),
|
|
1244
1326
|
},
|
|
1327
|
+
selfId,
|
|
1328
|
+
issuerAudience ?? "",
|
|
1245
1329
|
message,
|
|
1246
1330
|
maxWaitMs,
|
|
1247
1331
|
approvedActions,
|
|
@@ -1267,7 +1351,6 @@ function askAppTool(
|
|
|
1267
1351
|
return {
|
|
1268
1352
|
app: selfId,
|
|
1269
1353
|
routedVia: "local",
|
|
1270
|
-
...(note ? { note } : {}),
|
|
1271
1354
|
response: inline.response || "(no response)",
|
|
1272
1355
|
};
|
|
1273
1356
|
}
|
|
@@ -1276,7 +1359,7 @@ function askAppTool(
|
|
|
1276
1359
|
// same way the old unbounded `await config.askAgent(message)` did.
|
|
1277
1360
|
throw new Error(inline.error || "ask_app task failed.");
|
|
1278
1361
|
}
|
|
1279
|
-
return askAppInlineTaskResult(selfId, inline.taskId, inline
|
|
1362
|
+
return askAppInlineTaskResult(selfId, inline.taskId, inline);
|
|
1280
1363
|
},
|
|
1281
1364
|
};
|
|
1282
1365
|
}
|
|
@@ -1300,16 +1383,45 @@ function askAppStatusTool(
|
|
|
1300
1383
|
},
|
|
1301
1384
|
taskId: {
|
|
1302
1385
|
type: "string",
|
|
1303
|
-
description:
|
|
1386
|
+
description:
|
|
1387
|
+
"Legacy durable task id returned by ask_app. Use taskHandle when present.",
|
|
1388
|
+
},
|
|
1389
|
+
taskHandle: {
|
|
1390
|
+
type: "string",
|
|
1391
|
+
description:
|
|
1392
|
+
"Opaque task handle returned by ask_app. It preserves the original route across cold starts and discovery changes.",
|
|
1304
1393
|
},
|
|
1305
1394
|
},
|
|
1306
|
-
["taskId"],
|
|
1307
1395
|
),
|
|
1308
1396
|
readOnly: true,
|
|
1309
1397
|
parallelSafe: true,
|
|
1310
1398
|
run: async (args: Record<string, any>) => {
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1399
|
+
const suppliedTaskId = String(args.taskId ?? "").trim();
|
|
1400
|
+
const taskHandle = String(args.taskHandle ?? "").trim();
|
|
1401
|
+
if (!taskHandle && !suppliedTaskId) {
|
|
1402
|
+
throw new Error("ask_app_status requires 'taskHandle' or 'taskId'.");
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (taskHandle) {
|
|
1406
|
+
const verified = await verifyAskAppTaskHandle(taskHandle, {
|
|
1407
|
+
issuerApp: currentAppId(config),
|
|
1408
|
+
issuerAudience: askAppIssuerAudience(requestMeta) ?? "",
|
|
1409
|
+
organization: getRequestOrgId() ?? "",
|
|
1410
|
+
subject: getRequestUserEmail() ?? "anonymous",
|
|
1411
|
+
});
|
|
1412
|
+
const suppliedApp = String(args.app ?? "")
|
|
1413
|
+
.trim()
|
|
1414
|
+
.toLowerCase();
|
|
1415
|
+
if (
|
|
1416
|
+
(suppliedTaskId && suppliedTaskId !== verified.taskId) ||
|
|
1417
|
+
(suppliedApp && suppliedApp !== verified.route.app.toLowerCase())
|
|
1418
|
+
) {
|
|
1419
|
+
throw new Error("Invalid or expired ask_app task handle.");
|
|
1420
|
+
}
|
|
1421
|
+
return fetchAskAppA2ATask(verified.route, verified.taskId, taskHandle);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const taskId = suppliedTaskId;
|
|
1313
1425
|
|
|
1314
1426
|
// The no-derivable-origin ask_app fallback tracks its tasks in a
|
|
1315
1427
|
// process-local map, not the durable A2A store. Check it FIRST —
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { A2AClient } from "../a2a/client.js";
|
|
2
|
-
import type { AgentSkill } from "../a2a/types.js";
|
|
2
|
+
import type { AgentCard, AgentSkill } from "../a2a/types.js";
|
|
3
3
|
import { type DiscoveredAgent } from "./agent-discovery.js";
|
|
4
4
|
|
|
5
5
|
/** Matches the `<available-apps>` prompt block so both surfaces agree. */
|
|
@@ -16,6 +16,12 @@ export interface PeerCapabilities {
|
|
|
16
16
|
skills: AgentSkill[] | null;
|
|
17
17
|
cardDescription?: string;
|
|
18
18
|
error?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The full card from this same fetch, for callers that need more than
|
|
21
|
+
* skills/description (e.g. the peer-probe route's name/securitySchemes).
|
|
22
|
+
* Kept optional so existing consumers of this shape are unaffected.
|
|
23
|
+
*/
|
|
24
|
+
card?: AgentCard;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
function truncate(value: string, max: number): string {
|
|
@@ -35,6 +41,7 @@ export async function loadCapabilities(
|
|
|
35
41
|
skills: Array.isArray(card.skills) ? card.skills : [],
|
|
36
42
|
cardDescription:
|
|
37
43
|
typeof card.description === "string" ? card.description : undefined,
|
|
44
|
+
card,
|
|
38
45
|
};
|
|
39
46
|
} catch (error) {
|
|
40
47
|
return {
|
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
toolCallCacheKey,
|
|
75
75
|
getActiveRunForThreadAsync,
|
|
76
76
|
abortRunDurably,
|
|
77
|
+
abortTurnDurably,
|
|
77
78
|
subscribeToRun,
|
|
78
79
|
type ActionEntry,
|
|
79
80
|
} from "../agent/production-agent.js";
|
|
@@ -441,6 +442,19 @@ const generateTitleRateLimit = new Map<string, number[]>();
|
|
|
441
442
|
* so the common small-map case stays O(1). */
|
|
442
443
|
const RATE_LIMIT_SWEEP_THRESHOLD = 1000;
|
|
443
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Abort reasons that mean a human asked to stop, so `/runs/:id/abort` escalates
|
|
447
|
+
* to a turn-wide abort. Watchdog reasons (`no_progress`, `auto_stuck_retry`)
|
|
448
|
+
* stay single-run: they end a chunk the client believes is dead and must not
|
|
449
|
+
* kill a server-side continuation chain that is still making progress.
|
|
450
|
+
*/
|
|
451
|
+
export const USER_STOP_ABORT_REASONS = new Set([
|
|
452
|
+
"user",
|
|
453
|
+
"abort",
|
|
454
|
+
"user_stuck_cancel",
|
|
455
|
+
"user_stuck_retry",
|
|
456
|
+
]);
|
|
457
|
+
|
|
444
458
|
/**
|
|
445
459
|
* Pick the URL allowlist to enforce for a `${keys.NAME}` reference used by
|
|
446
460
|
* the agent fetch tool.
|
|
@@ -4572,6 +4586,9 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
|
|
|
4572
4586
|
// the cross-isolate abort + terminal event to be durable. Returning
|
|
4573
4587
|
// early lets Retry collide with the still-running row.
|
|
4574
4588
|
await abortRunDurably(runId, reason);
|
|
4589
|
+
if (USER_STOP_ABORT_REASONS.has(reason)) {
|
|
4590
|
+
await abortTurnDurably(runId, reason);
|
|
4591
|
+
}
|
|
4575
4592
|
return { ok: true };
|
|
4576
4593
|
}
|
|
4577
4594
|
|
|
@@ -79,7 +79,7 @@ const HIDDEN_FIRST_PARTY_AGENT_IDS = new Set([
|
|
|
79
79
|
"workbench",
|
|
80
80
|
]);
|
|
81
81
|
|
|
82
|
-
function normalizeAgentId(id: string): string {
|
|
82
|
+
export function normalizeAgentId(id: string): string {
|
|
83
83
|
const normalized = id.trim().toLowerCase();
|
|
84
84
|
if (
|
|
85
85
|
normalized === "image" ||
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { resolveA2ACallerAuth } from "../a2a/caller-auth.js";
|
|
4
|
+
import { A2AClient } from "../a2a/client.js";
|
|
5
|
+
import {
|
|
6
|
+
loadCapabilities,
|
|
7
|
+
type PeerCapabilities,
|
|
8
|
+
} from "./agent-capabilities.js";
|
|
9
|
+
import type { DiscoveredAgent } from "./agent-discovery.js";
|
|
10
|
+
|
|
11
|
+
const AUTH_PROBE_TIMEOUT_MS = 6_000;
|
|
12
|
+
/** Matches CARD_CONCURRENCY in agent-capabilities.ts — bounds simultaneous
|
|
13
|
+
* outbound probes the same way card loading already does. */
|
|
14
|
+
const PROBE_CONCURRENCY = 8;
|
|
15
|
+
|
|
16
|
+
export interface PeerProbeResult {
|
|
17
|
+
url: string;
|
|
18
|
+
reachable: boolean;
|
|
19
|
+
name?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
securitySchemes?: string[];
|
|
22
|
+
/** Absent (not false) whenever the auth check never ran or never resolved. */
|
|
23
|
+
authorized?: boolean;
|
|
24
|
+
authError?: string;
|
|
25
|
+
publicSkills?: number;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PeerProbeDeps {
|
|
30
|
+
loadCapabilities: (agent: DiscoveredAgent) => Promise<PeerCapabilities>;
|
|
31
|
+
resolveCallerAuth: typeof resolveA2ACallerAuth;
|
|
32
|
+
createClient: (
|
|
33
|
+
baseUrl: string,
|
|
34
|
+
apiKey?: string,
|
|
35
|
+
options?: { requestTimeoutMs?: number; fallbackApiKeys?: string[] },
|
|
36
|
+
) => Pick<A2AClient, "getTask">;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const defaultPeerProbeDeps: PeerProbeDeps = {
|
|
40
|
+
loadCapabilities,
|
|
41
|
+
resolveCallerAuth: resolveA2ACallerAuth,
|
|
42
|
+
createClient: (baseUrl, apiKey, options) =>
|
|
43
|
+
new A2AClient(baseUrl, apiKey, options),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Probe one peer: does its agent card load (reachability), and separately,
|
|
48
|
+
* will OUR calls to it authenticate. These two questions must never collapse
|
|
49
|
+
* into one boolean — an unreachable peer can't tell us anything about auth,
|
|
50
|
+
* so `authorized` stays absent rather than `false` in that case, and a
|
|
51
|
+
* transport failure on the auth-only call (timeout, DNS, 5xx) must never be
|
|
52
|
+
* reported as an auth rejection either.
|
|
53
|
+
*/
|
|
54
|
+
export async function probePeerAgent(
|
|
55
|
+
agent: DiscoveredAgent,
|
|
56
|
+
deps: PeerProbeDeps = defaultPeerProbeDeps,
|
|
57
|
+
): Promise<PeerProbeResult> {
|
|
58
|
+
const capabilities = await deps.loadCapabilities(agent);
|
|
59
|
+
if (capabilities.skills === null || !capabilities.card) {
|
|
60
|
+
// Unreachable (includes malformed/SSRF-blocked URLs, which the caller
|
|
61
|
+
// reclassifies into a 400 by checking for the "SSRF blocked:" prefix).
|
|
62
|
+
// `authorized` is intentionally omitted — reachability and auth are
|
|
63
|
+
// independent questions, and we have no evidence either way here.
|
|
64
|
+
return {
|
|
65
|
+
url: agent.url,
|
|
66
|
+
reachable: false,
|
|
67
|
+
error: capabilities.error ?? "unreachable",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const card = capabilities.card;
|
|
72
|
+
const result: PeerProbeResult = {
|
|
73
|
+
url: agent.url,
|
|
74
|
+
reachable: true,
|
|
75
|
+
name: card.name,
|
|
76
|
+
description: capabilities.cardDescription,
|
|
77
|
+
securitySchemes: card.securitySchemes
|
|
78
|
+
? Object.keys(card.securitySchemes)
|
|
79
|
+
: undefined,
|
|
80
|
+
publicSkills: capabilities.skills.length,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const auth = await deps.resolveCallerAuth();
|
|
84
|
+
const client = deps.createClient(agent.url, auth.apiKey, {
|
|
85
|
+
requestTimeoutMs: AUTH_PROBE_TIMEOUT_MS,
|
|
86
|
+
fallbackApiKeys: auth.apiKeyFallbacks,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// An id that cannot exist. A "task not found" JSON-RPC error proves the
|
|
91
|
+
// peer authenticated us and ran our request; a 401 proves it rejected us;
|
|
92
|
+
// anything else is a transport failure this call can't resolve either way.
|
|
93
|
+
await client.getTask(`probe-${crypto.randomUUID()}`, {
|
|
94
|
+
requestTimeoutMs: AUTH_PROBE_TIMEOUT_MS,
|
|
95
|
+
});
|
|
96
|
+
result.authorized = true;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
if (/A2A request failed \(401\)/.test(message)) {
|
|
100
|
+
result.authorized = false;
|
|
101
|
+
result.authError = "401";
|
|
102
|
+
} else if (/A2A error \(-?\d+\): task not found/i.test(message)) {
|
|
103
|
+
result.authorized = true;
|
|
104
|
+
} else {
|
|
105
|
+
// Timeout, DNS failure, non-401 HTTP status, etc. The card fetch above
|
|
106
|
+
// already proved reachability, so this must not be reported as
|
|
107
|
+
// `reachable: false`, and an unresolved auth outcome must not be
|
|
108
|
+
// reported as `authorized: false` either — leave it absent.
|
|
109
|
+
result.authError = message;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function probeAllPeerAgents(
|
|
117
|
+
agents: DiscoveredAgent[],
|
|
118
|
+
deps: PeerProbeDeps = defaultPeerProbeDeps,
|
|
119
|
+
): Promise<Array<PeerProbeResult & { id: string }>> {
|
|
120
|
+
const results: Array<PeerProbeResult & { id: string }> = [];
|
|
121
|
+
for (let i = 0; i < agents.length; i += PROBE_CONCURRENCY) {
|
|
122
|
+
const batch = agents.slice(i, i + PROBE_CONCURRENCY);
|
|
123
|
+
results.push(
|
|
124
|
+
...(await Promise.all(
|
|
125
|
+
batch.map(async (agent) => ({
|
|
126
|
+
id: agent.id,
|
|
127
|
+
...(await probePeerAgent(agent, deps)),
|
|
128
|
+
})),
|
|
129
|
+
)),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
@@ -1569,6 +1569,73 @@ export function createCoreRoutesPlugin(
|
|
|
1569
1569
|
// middleware any plugin's route can possibly land behind, regardless of
|
|
1570
1570
|
// plugin init ordering.
|
|
1571
1571
|
|
|
1572
|
+
// Peer reachability + auth probe for the settings UI. Deliberately
|
|
1573
|
+
// separate from `${P}/agents` (discovery) — this route makes live
|
|
1574
|
+
// network calls to the peer, so it is session-gated and answers one
|
|
1575
|
+
// peer (`?url=`) or every registered peer (no query) via `discoverAgents`.
|
|
1576
|
+
//
|
|
1577
|
+
// MUST be mounted BEFORE `${P}/agents` below: h3's `.use()` matches by
|
|
1578
|
+
// path prefix, and that handler always returns a value (never calls
|
|
1579
|
+
// `next()`), so it would swallow `/agents/probe` requests before they
|
|
1580
|
+
// ever reached this route if registered second (same hazard as the A2A
|
|
1581
|
+
// `_process-task` route vs. its `/a2a` catch-all — see a2a/server.ts).
|
|
1582
|
+
getH3App(nitroApp).use(
|
|
1583
|
+
`${P}/agents/probe`,
|
|
1584
|
+
defineEventHandler(async (event) => {
|
|
1585
|
+
if (getMethod(event) !== "GET") {
|
|
1586
|
+
setResponseStatus(event, 405);
|
|
1587
|
+
return { error: "Method not allowed" };
|
|
1588
|
+
}
|
|
1589
|
+
const session = await getSession(event).catch(() => null);
|
|
1590
|
+
if (!session?.email) {
|
|
1591
|
+
setResponseStatus(event, 401);
|
|
1592
|
+
return { error: "Authentication required" };
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
return runWithRequestContext(
|
|
1596
|
+
{ userEmail: session.email, orgId: session.orgId ?? undefined },
|
|
1597
|
+
async () => {
|
|
1598
|
+
const { probePeerAgent, probeAllPeerAgents } =
|
|
1599
|
+
await import("./agent-peer-probe.js");
|
|
1600
|
+
const query = getRequestURL(event).searchParams;
|
|
1601
|
+
const urlParam = query.get("url");
|
|
1602
|
+
|
|
1603
|
+
if (urlParam === null) {
|
|
1604
|
+
const selfAppId = query.get("selfAppId") ?? undefined;
|
|
1605
|
+
const { discoverAgents } = await import("./agent-discovery.js");
|
|
1606
|
+
const agents = await discoverAgents(selfAppId);
|
|
1607
|
+
const results = await probeAllPeerAgents(agents);
|
|
1608
|
+
return { results };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (!urlParam.trim()) {
|
|
1612
|
+
setResponseStatus(event, 400);
|
|
1613
|
+
return { error: "url is required" };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const result = await probePeerAgent({
|
|
1617
|
+
id: "probe",
|
|
1618
|
+
name: urlParam,
|
|
1619
|
+
description: "",
|
|
1620
|
+
url: urlParam,
|
|
1621
|
+
color: "",
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
// Reachability and auth are independent, but a malformed/SSRF-blocked
|
|
1625
|
+
// URL is a caller input error, not a peer that failed to answer — the
|
|
1626
|
+
// one case where the probe's "unreachable" result is reclassified into
|
|
1627
|
+
// a 400 instead of a 200 with `reachable: false`.
|
|
1628
|
+
if (result.error?.startsWith("SSRF blocked:")) {
|
|
1629
|
+
setResponseStatus(event, 400);
|
|
1630
|
+
return { url: urlParam, error: result.error };
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
return result;
|
|
1634
|
+
},
|
|
1635
|
+
);
|
|
1636
|
+
}),
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1572
1639
|
// Agent discovery primitive — shared by headless CLI/A2A surfaces and
|
|
1573
1640
|
// UI shells that need to show connected peer apps without depending on
|
|
1574
1641
|
// the chat route namespace.
|